Using AWS JWT authorizers with Auth0
1. The situation
Say that we have decided to use Auth0 for authorization in our architecture. We want to give users with different sets of permissions access to different levels based on their permissions.
We want a quick and cheap solution to authorize user requests with the external HTTP API (not REST API) created with API Gateway. HTTP APIs are cheaper than REST APIs and are easier to set up but have fewer configuration options.
After the user has logged in to the application, we want API Gateway to validate their JSON Web Token (JWT) that Auth0 had issued.
One of the requirements is that other services (and eventually 3rd party applications) could also use the API Gateway and the new authorization solution to call our microservice.
2. A quick solution
We can set up a JWT authorizer for the given route, which will automatically check if the token is valid. After inspecting and extracting the relevant properties, the authorizer will validate the token permissions with Auth0.
The best selling point of this solution is that it’s cheap (about 1/6th of the price of the REST API) and that we don’t have to set up any custom authorizers.
A custom authorizer is a Lambda function that contains custom logic to verify the permissions of the caller. Each time API Gateway invokes the function to authorize the request, the custom (Lambda) authorizer will run (caching is available). A Lambda authorizer would cost us money, especially if the route accepts heavy traffic. The developer’s time to write the function code can also be a cost factor.
We won’t have these issues with JWT authorizers. We can easily set up a JWT authorizer. We don’t write custom codes as everything comes out of the box. JWT authorizers will inspect the scope
property of the token, so we should ensure that it will contain this claim.
3. Pre-requisites
This post assumes that you will have everything set up in Auth0. It includes:
- Having a user (with a username and password) created in the User Management section
- Assigning the user some permissions - I will use the
read:user
permission for demonstration - Having a API created that has at least
read:user
permission - more is OK to test various users with different permissions - Having an Application (client or app client) that supports
Password
andClient Credentials
grant types - You have authorized the application to request access tokens from the API.
This post won’t explain how to set all of these. I will link an article from an Auth0 staff member which explains the process in detail at the end of this post.
4. Steps to set up the authorizer
Let’s go over how to set up a JWT authorizer for an HTTP API.
Say we have an HTTP API with a route called /test
. It will accept GET
requests for the sake of simplicity.
I also created a simple Lambda function that has the following code:
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('JWT test backend lambda'),
};
return response;
};
For this post, this function will be the integration of the GET /test
route. It’s irrelevant what the integration is; I just chose a Lambda function because it’s easy to set up. In reality, the backend integration can be anything the HTTP API supports.
4.1. Creating and attaching the authorizer
If we go to the Authorization menu on the left, we will find the /test/
route and a button called Create and attach an authorizer.
The default option will be JWT, and this is what we need! The Name can be anything, so enter something friendly there.
The Identity source field will define which header the authorizer will investigate. By default, this is the Authorization
header. Therefore the request should include the access token (generated by Auth0) as the value of the Authorization
header. We can leave it as is. If you decide to change it, ensure that you include that header in the request.
The Issuer URL is the Auth0 domain. It could be the default tenant domain like https://DOMAIN.auth0.com
or a custom domain like https://login.DOMAIN.com
. You can find it in the Application section under the Basic Information part in Auth0.
The last field for the authorizer is the Audience.
When you created the API in Auth0, you should have specified a unique identifier that must be of a valid URI format. This ID can be anything and doesn’t have to exist in the DNS database. For example, https://test-api-for-jwt-authorizer.example.com
clearly doesn’t exist. But it’s not a problem because no one will ever call it. It only serves identification purposes.
We should paste this identifier in the Audience box for the JWT authorizer.
4.2. Adding authorization scopes
Authorization scopes are special permissions necessary to receive a successful response when we can call the endpoint (/test
in this example). If the request token contains the required scope (permission), then the JWT authorizer will allow it to continue to the integration. If the scope is not present, the authorizer denies the request.
Suppose the endpoint requires the read:user
scope, so let’s add it to the authorizer. We can add more scopes if needed, but for this example, read:user
will be sufficient.
To invoke the API endpoint, the test user in Auth0 should have the read:user
permission. We should have already set it up in the Auth0 dashboard.
5. Scenarios
The JWT authorizer is ready to use! We can use Postman or curl to test the endpoint and the authorizer.
5.1. How it works
First, we will get the token from Auth0. We will investigate two scenarios: service-to-service (or machine-to-machine) and username/password authorization.
In both cases, we will make a POST
request to the https://AUTH0_DOMAIN/oauth/token
endpoint in Auth0 and receive the access token in the response.
Next, we will place the token in the Authorization
header for our GET /test
request.
Authorization: Bearer <TOKEN HERE>
The JWT authorizer in the API Gateway will check if the token is valid, extract the value(s) of the scope
property from it, and then compare these values to the scopes we have set in the authorizer. API Gateway will also validate the token’s claims with Auth0.
5.2. Service-to-service authorization
We can use this type of authorization when we want to authorize the communication between two microservices. When one service calls another, the API Gateway of the callee can utilize the JWT authorizer to validate the token.
In this case, the payload for the token request looks like this:
{
"audience":"https://test-api-for-jwt-authorizer.example.com",
"grant_type":"client_credentials",
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET"
}
audience
is the API identifier we set up earlier in Auth0. It should have a valid URI format.
The grant_type
in this case will be client_credentials
. It means we are providing the client_id
and the client_secret
for the application (app client) we have in Auth0. The application will call the Auth0 authorization server on behalf of the authorizing service.
In this case, the API accepts three permissions (read:user
, write:user
, and read:appointments
) and we assigned read:user
to the app client. By providing the client ID, client secret, and the client_credentials
grant type in the token request, Auth0 will know that it will have to include the read:user
permission in the token.
If we now invoke the /test
endpoint, we should get a successful response.
5.3. User authorization
The token request will look slightly different if we have multiple users of various access levels for their relevant permissions.
Let’s assume that the user has read:user
permission and nothing else. We set up the user permissions when we attached a role to the user in Auth0.
The payload for the https://AUTH0_DOMAIN/oauth/token
call will look like this:
{
"username": "USERNAME",
"password": "PASSWORD",
"audience": "https://test-api-for-jwt-authorizer.example.com",
"grant_type":" password",
"client_id": "CLIENT ID",
"client_secret": "CLIENT SECRET",
"scope": "delete:user read:user write:user read:appointments"
}
The user will request the token with their username
and password
. These credentials are not necessarily the same as the Auth0 login credentials! Auth0 is an identity provider, so we will have to create each user inside our account who needs access tokens.
grant_type
is password
here because we provide the user’s username and password.
We have to include the scope
in the request. We demand these scopes be in the access token. If we don’t provide the scope
property with the permissions we want, the token will not contain the scope
property, and the JWT authorizer will deny the request.
As it turns out, we can request more permissions than the user is entitled to. Auth0 will take the intersection of the request scopes (in the code snippet) and the permissions assigned to the user (in the image). In this case, read:user
is the only matching scope in the request and in the user’s permission set. Auth0 will only include this permission in the scope
property of the token.
If we removed read:user
from the request, we would still get the token. This time the scope
property wouldn’t be there. It is because the intersection of the request scopes and the user permissions would be an empty set.
This way, it’s possible to include all possible permissions in any user requests, and Auth0 will select the ones the user owns.
6. Summary
HTTP APIs in API Gateway offers the JWT authorizer type, which inspects the scope
property of the provided token. The request can proceed to the backend if the token contains the relevant scopes. If it does not, the authorizer will deny the request.
We have to provide the Auth0 domain and API identifier in API Gateway so that the authorizer knows where to check for the validity and integrity of the provided token.
7. Further reading
Controlling access to HTTP APIs with JWT authorizers - General information about JWT authorizers
Securing AWS HTTP APIs with JWT Authorizers - Same topic with more detailed description on the Auth0 part
Choosing between REST APIs and HTTP APIs - Comparison with lots of tables