Controlling access to your AWS API Gateway HTTP API with Auth0
Tags: aws, api, auth0, jwt
Published: 2020-05-28 18:16:52


> Want to skip the details? Try out the [online demo](https://aws-api-gateway-jwt-auth0.now.sh/).

## Introduction

A few weeks ago [AWS API Gateway HTTP APIs](https://aws.amazon.com/blogs/compute/building-better-apis-http-apis-now-generally-available/) became generally available - offering a simpler, faster and cheaper way to build APIs.
One of the capabilities that has been simplified is the whole authorization story, which is what we'll be covering in this blog post.

For the traditional REST APIs you would often write your own [Custom Authorizer](https://github.com/auth0-samples/jwt-rsa-aws-custom-authorizer) using a Lambda function to support JWT authorization (mainly in the context of OpenID Connect and OAuth 2.0).
This is no longer needed for HTTP APIs, which offers a [JWT authorizer](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-jwt-authorizer.html) out of the box.

![AWS API Gateway JWT Authorizer](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/banner.jpeg)

What this means is that you can configure your own JWT Authorizer by providing 3 simple settings instead of having to deploy and maintain your custom authorizers:

- `identitySource`: Which refers to where the token can be found (eg: the `Authorize` header)
- `issuer`: The issuer URI of the Identity Provider (eg: `https://acme.auth0.com/`)
- `audience`: The audience of your API

## Auth0 as a JWT Authorizer

Let's see how we can configure Auth0 as a JWT Authorizer. We'll be building a simple API returning colors with public endpoints and private endpoints, requiring the user to authenticate first.

### Configuring your Auth0 account

In your account you'll want to represent your HTTP API as an API in Auth0, which you'll need to give a name and an identifier. The identifier is what will end up being the `audience` of your JWT Authorizer.

![Configuring API settings in Auth0](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/api-settings.jpeg)

I'm also going to create a "Single Page Application" under **Applications** since I'll write a small React application to interact with the HTTP API. Also here the configuration is relatively simple.

- `Allowed Callback URLs`: **http://localhost:3000**
- `Allowed Web Origins`: **http://localhost:3000**
- `Allowed Origins (CORS)`: **http://localhost:3000**

> We configure the application with **http://localhost:3000** so we can test everything locally.

And the last thing we'll do on the Auth0 side is write [a rule](https://auth0.com/docs/rules) which will add some custom claims to the `access_token`:

```js
function (user, context, callback) {
  context.accessToken['http://sandrino/roles'] =  [ 'admin', 'member' ];
  context.accessToken['http://sandrino/email'] =  user.email;
  context.accessToken['http://sandrino/email_verified'] =  user.email_verified.toString();
  return callback(null, user, context);
}
```

And that's actually all there is to it. Let's move on to the HTTP API.

### Configuring your HTTP API

There are plenty of resources out there ([example on the auth0.com blog](https://auth0.com/blog/securing-aws-http-apis-with-jwt-authorizers/)) which show you how to configure a JWT Authorizer in the AWS console, so in this post we'll use [the Serverless framework](https://www.serverless.com/) instead.

In the configuration below you'll see a few things:

- CORS is enabled for my API because we want the browser to be able to interact with it
- Auth0 was configured as an authorizer, by simply providing the Issuer URL and the audience of the API we created in the Auth0 dashboard
- We have two endpoints: `/colors` which is public (no authorizer configured), and `/my/profile` which will the request to be authorized

```yaml
service: http-api-jwt-example

provider:
  name: aws
  runtime: nodejs12.x
  httpApi:
    cors:
      allowedHeaders:
        - Content-Type
        - Authorization
      allowedMethods:
        - GET
        - OPTIONS
      allowedOrigins:
        - http://localhost:3000
    payload: '2.0'
    authorizers:
      accessTokenAuth0:
        identitySource: $request.header.Authorization
        issuerUrl: https://sandrino.auth0.com/
        audience:
          - urn:colors-api

functions:
  # List all colors (public endpoint)
  getColors:
    handler: handler.colors
    events:
      - httpApi:
          method: GET
          path: /colors
  # List my profile (requires authorization)
  myProfile:
    handler: handler.myProfile
    events:
      - httpApi:
          method: GET
          path: /my/profile
          authorizer:
            name: accessTokenAuth0
```

If we apply this configuration you'll see a new JWT Authorizer show up in your AWS console:

![AWS API Gateway JWT Authorizer](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/authorizer.jpeg)

> When using Custom Domains in Auth0, you should configure that domain as the `issuerUrl`. So for example, you should use `https://auth.sandrino.dev/` instead of `https://sandrino.auth0.com/`.

When a JWT Authorizer is configured for a route you won't have to worry about parsing and validating the token. If a valid token is provided, the claims will be available in the `event` - otherwise the request will fail.
Below is an example of a function accessing the claims provided by the JWT Authorizer and also extracting any custom claims we might have added (using Auth0 Rules):

```js
module.exports.myProfile = async (event) => {
  const claims = event.requestContext.authorizer.jwt.claims;
  return {
    id: claims.sub,
    roles: claims['http://sandrino/roles'],
    claims
  };
};
```

## Calling the API from the browser

We'll also create a simple Next.js application which use [auth0-spa-js](https://auth0.com/docs/libraries/auth0-spa-js) to sign in from the browser.

```js
import { Auth0Client } from '@auth0/auth0-spa-js';

export default new Auth0Client({
  domain: 'https://sandrino.auth0.com', // Should be your Auth0 domain or your custom domain if you have configured it
  client_id: 'z9p3mE4Oc1PN4XKooapkPpn22nRONdJC',
  audience: 'urn:colors-api', // This should be the audience of the API you configured
  scope: 'openid profile email'
});
```

Our application will have a login button:

```js
login = async () => {
  await auth0.loginWithRedirect({
    redirect_uri: window.location.href
  });
};
```

And after the user has signed in, we'll use a React hook to fetch the data from our API:

```js
export default () => {
  const { data, error, isPending, get } = useApi(process.env.API_GATEWAY_BASE_URL);

  const getColors = () => get('/colors', { auth: false });
  const getMyProfile = () => get('/my/profile');

  return (
    <>
      <Stack mb={5} isInline spacing={4}>
        <Button size="md" onClick={getColors}>
          List All Colors
        </Button>
        <Button size="md" onClick={getMyProfile}>
          My Profile
        </Button>
      </Stack>
      <Snippet
        code={
          (isPending && '// Loading...') ||
          (error && JSON.stringify(error, null, 2)) ||
          (data && JSON.stringify(data, null, 2)) ||
          '// Press one of the buttons above to call the API Gateway'
        }
        language="json"
      />
    </>
  );
};
```

> `useApi` is a custom React hook which retrieves the `access_token` from `auth0-spa-js` and attaches it to the Authorization header of the HTTP request. The implementation can be found [in the sample project](https://github.com/sandrinodimattia/code-samples/blob/master/aws-api-gateway-jwt-sample/web-app/lib/use-api.js).

If we try this out we'll notice that unauthenticated calls fail:

![Unauthenticated Call](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/not-authenticated.jpeg)

But when we do sign in through Auth0 and the `access_token` is provided in the HTTP request the JWT Authorizer will allow the request to go through to our handler:

![Authenticated Call](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/authenticated.jpeg)

> You'll notice that the issuer here is set to `https://auth.sandrino.dev/` since I'm doing all of my tests with my Auth0 Custom Domain

## Authorization Scopes and Role Based Access Control

On each route you also have the option to configure **Authorization Scopes**, allowing you to define which `scope` should be present in the token to access the API route. In the example below you can see that I'm now requiring the `read:colors` scope for a new endpoint:

![Authorization Scopes](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/authorization-scopes.jpeg)

If you're using the Serverless framework, you can just add the required scope to your route:

```yaml
myColors:
  handler: handler.myColors
  events:
    - httpApi:
        method: GET
        path: /my/colors
        authorizer:
          name: accessTokenAuth0
          scopes:
            - read:colors
```

No code changes are required to your Lambda function, everything is handled by the JWT Authorizer. The only change you'll need to make is to request this scope on the client side:

```js
import { Auth0Client } from '@auth0/auth0-spa-js';

export default new Auth0Client({
  domain: 'https://sandrino.auth0.com', // Should be your Auth0 domain or your custom domain if you have configured it
  client_id: 'z9p3mE4Oc1PN4XKooapkPpn22nRONdJC',
  audience: 'urn:colors-api', // This should be the audience of the API you configured
  scope: 'openid profile email read:colors' // We're requesting an additional scope here
});
```

Where it gets really interesting is the link with Auth0's Role Based Access Control features, where we can control access to certain scopes using roles and permissions. Let's look at an example. On the API we'll configure a new `read:colors` permission:

![Configuring API scopes in Auth0](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/api-scopes.jpeg)

As part of this we'll also want to enable authorization policies for our API (the **Enable RBAC** toggle). This will require the user to have the right set of permissions:

![Configuring API scopes in Auth0](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/enable-rbac.jpeg)

With this setting enabled, the user needs to have the `read:colors` permission otherwise the scope will not be allowed. So even if a client requests `openid profile email read:colors`, the token will end up looking like this:

```js
{
  "http://sandrino/email": "sandrino@auth0.com",
  "http://sandrino/email_verified": "true",
  "iss": "https://auth.sandrino.dev/",
  "sub": "auth0|593b80d5f8a341400599ce4f",
  "aud": [
    "urn:colors-api",
    "https://sandrino.auth0.com/userinfo"
  ],
  "iat": 1590666735,
  "exp": 1590753135,
  "azp": "z9p3mE4Oc1PN4XKooapkPpn22nRONdJC",
  "scope": "openid profile email"
}
```

> Notice how the `read:colors` scope is not there, even though it was requested by the client

And as a result, a call to the new endpoint we created will fail because our token is missing the right scope:

![Request failed due to missing scope](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/my-colors-failed.jpeg)

Let's go ahead and create a Role in Auth0 to which we'll add the `read:colors` permission. By adding the permission to this role, any user with this role assigned will be able to receive the `read:colors` scope.

![Create Role](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/create-role.jpeg)

After creating the roles we can now assign it to a user:

![Role assigned to user](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/role-assigned.jpeg)

Now when the user signs in you'll notice an important difference, the `read:colors` scope is available.

```js
{
  "http://sandrino/email": "sandrino@auth0.com",
  "http://sandrino/email_verified": "true",
  "iss": "https://auth.sandrino.dev/",
  "sub": "auth0|593b80d5f8a341400599ce4f",
  "aud": [
    "urn:colors-api",
    "https://sandrino.auth0.com/userinfo"
  ],
  "iat": 1590678148,
  "exp": 1590764548,
  "azp": "z9p3mE4Oc1PN4XKooapkPpn22nRONdJC",
  "scope": "openid profile email read:colors"
}
```

When we try to access the new endpoint ... it works! With the required scope we are able to make a call to the new route:

![Request succeeded](https://sandrino.dev/assets/blog/aws-api-gateway-jwt-auth0/my-colors-success.jpeg)

> You users will need to retieve a new access token from Auth0 before such a change has effect in your application. This happens by signing in again, through [Silent Authentication](https://auth0.com/docs/api-auth/tutorials/silent-authentication) (using an iframe) or by using a Refresh Token.

## ✅ Success!

As you've seen, with JWT Authorizers it becomes very easy to cover the authorization aspect of your API Gateway. And while this article has focused on end-user authentication, the same concepts of `access_tokens` and `scope` also apply to [applications](https://auth0.com/blog/using-m2m-authorization/) talking to your API on their own behalf.

The full sample of the Serverless framework project and the Next.js application can be found on [GitHub](https://github.com/sandrinodimattia/code-samples/tree/master/aws-api-gateway-jwt-sample).