Using AWS JWT authorizers with Auth0
Tags: aws, security, serverless
Published: 2022-07-28 05:12:46

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 <a href="https://auth0.com/" class="link" target="_blank" rel="noopener noreferrer">Auth0</a> 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 <a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" class="link" target="_blank" rel="noopener noreferrer">HTTP API</a> (not REST API) created with <a href="https://aws.amazon.com/api-gateway/" class="link" target="_blank" rel="noopener noreferrer">API Gateway</a>. 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 <a href="https://jwt.io/" class="link" target="_blank" rel="noopener noreferrer">JSON Web Token (JWT)</a> 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 <a href="https://aws.amazon.com/lambda" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> 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 <a href="https://aws.amazon.com/lambda/" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> function that has the following code:

```js
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.

![Create a JWT authorizer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d0g135n7b1otb1x0eemn.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s1g1zdsn0cmr8tf2ayfz.png)

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 <a href="https://www.postman.com/" class="link" target="_blank" rel="noopener noreferrer">Postman</a> or <a href="https://curl.haxx.se/" class="link" target="_blank" rel="noopener noreferrer">curl</a> 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:

```json
{
  "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.

![Application permissions in Auth0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ts7hui9vgk84klyq0vpb.png)

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:

```json
{
  "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.

![User permissions in Auth0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zd67gsirrt0nptv1aejb.png)

`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

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html" class="link" target="_blank" rel="noopener noreferrer">Controlling access to HTTP APIs with JWT authorizers</a> - General information about JWT authorizers

<a href="https://auth0.com/blog/securing-aws-http-apis-with-jwt-authorizers/" class="link" target="_blank" rel="noopener noreferrer">Securing AWS HTTP APIs with JWT Authorizers</a> - Same topic with more detailed description on the Auth0 part

<a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" class="link" target="_blank" rel="noopener noreferrer">Choosing between REST APIs and HTTP APIs</a> - Comparison with lots of tables