DEV Community

Arpad Toth for AWS Community Builders

Posted on • Edited on • Originally published at arpadt.com

Using AWS JWT authorizers with Auth0

We can quickly set up the built-in JWT authorizer in API Gateway to validate Auth0 tokens. The authorizer will allow requests to reach the backend integration if the token contains the pre-defined scope and it will deny the rest of the invocations.

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 and Client Credentials grant type
  • 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;
};
Enter fullscreen mode Exit fullscreen mode

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.

Create a JWT authorizer

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.

Add scopes

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>
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

Application permissions in Auth0

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"
}
Enter fullscreen mode Exit fullscreen mode

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.

User permissions in Auth0

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

Top comments (0)