Token Authorizers with APIGatewayV2 (tricks APIGWv1 doesn't want you to know)
Tags: aws, serverless, auth0
Published: 2020-08-06 13:00:19

I came across a problem at work where we want to use APIGateway Version 2... but there isn't a clear example for how to use Authorizers with it.

API Gateway version 1 has constructs available for authorizers, but API Gateway v2 has other very useful features.  For example... API GatewayV2 can generate an OpenAPI spec from the deployed API... ~~API Gateway V1 can't do that...~~ *Edit: Apparently the aws cli command differs between versions. `aws apigateway get-export` vs `aws apigatewayv2 export-api`*

![Alt Text](https://media.giphy.com/media/1CNsm9ZkHF0m4/giphy.gif)
<figcaption>Choose wisely...</figcaption>

Let's get around that.  For reference, this is what I have installed locally:

```bash
$ node --version
v12.13.1
$ npm --version
6.12.1
$ cdk --version
1.56.0 (build c1c174d)
```

My code for this article lives here...

{% github martzcodes/blog-cdk-streams no-readme %}

# Table of Contents

* [Auth0 Setup](#auth0)
* [CDK Setup](#cdk)
* [Let's try it](#try)
* [One last thing](#gotcha)

# Auth0 Setup <a name="auth0"></a>

In this example I'm going to use Auth0.  Plenty of other ways to do this but I've found Auth0 to be very easy to use and the free-tier is plenty for a demo like this.

Sign up for a free Auth0 account. New accounts Auth0 will automatically create a dummy API and application for you, which is useful but there's a quicker route for demo purposes...

Create a machine to machine application!  Click Create Application and Choose the 4th one...

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/28frdyaeshafxj1isvrx.png)

Choose the already-existing API (the example one Auth0 created for you)...

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/i8re9nvz47jtkyooi3wz.png)

The scopes don't really matter for this demo... you can skip those and click "Authorize".

![Alt Text](https://media.giphy.com/media/vEcyRJ5yysqk/giphy.gif)

# CDK Setup <a name="cdk"></a>

The code for this demo is fairly boring.  [This commit](https://github.com/martzcodes/blog-cdk-apigwv2-token/commit/9dd03617ef8879341a21251789a50f799f42d134) constitutes the actual stuff we'll set up and it's only 41 lines of code added... most of it boilerplate.

First setup the project:

```bash
$ cdk init app --language typescript
...
$ npm i @aws-cdk/aws-apigatewayv2 @aws-cdk/aws-lambda @aws-cdk/aws-lambda-nodejs --save
```

Next... the actual good bits.

Create a dead simple lambda function that logs the event and returns a 200:

```typescript
export const handler = async (event: any): Promise<any> => {
  console.log(JSON.stringify(event));

  return {
    statusCode: 200,
    headers: {},
    body: "yeah it worked",
  };
};
```

Next add the lambda and api gateway to the stack:

```typescript
    const someLambda = new NodejsFunction(this, "someLambdaFunction", {
      entry: `${__dirname}/some-lambda.ts`,
      handler: "handler",
      runtime: Runtime.NODEJS_12_X
    });
    const httpApi = new HttpApi(this, "AuthorizedApi");
    const someIntegration = new LambdaProxyIntegration({
      handler: someLambda,
    });
    const routes = httpApi.addRoutes({
      path: "/",
      methods: [HttpMethod.GET],
      integration: someIntegration,
    });
```

At this point, if you were to `cdk deploy` the app and make a GET request to the endpoint... you'd get "yeah it worked" as a response because there isn't an authorizer to check anything.

So let's add the authorizer!

![Alt Text](https://media.giphy.com/media/ChkynEc2Y0f5e/giphy.gif)

We're using the [CfnAuthorizer](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigatewayv2.CfnAuthorizer.html)

```typescript
    const authorizer = new CfnAuthorizer(this, "SomeAuthorizer", {
      apiId: httpApi.httpApiId,
      authorizerType: "JWT", // HAS TO BE JWT FOR HTTP APIs !?!
      identitySource: ["$request.header.Authorization"],
      name: "some-authorizer",
      jwtConfiguration: {
        issuer: "https://martzcodes.us.auth0.com/",
        audience: ["https://martzcodes.us.auth0.com/api/v2/"],
      },
    });
```

Which is great and all... but it's _actually not connected to anything_.  There aren't really any doc examples either.  At the time of this writing... if you do [this search on github](https://github.com/search?q=%22new+CfnAuthorizer%22&type=Code) there are less than 40 results that do this!

In order to "attach" the authorizer to the api gateway endpoint you need to make sure it ends up in the cloudformation template that cdk generates to deploy the app... to do that you can use something like:


```typescript
    routes.forEach((route) => {
      const routeCfn = route.node.defaultChild as CfnRoute;
      routeCfn.authorizerId = authorizer.ref;
      routeCfn.authorizationType = "JWT"; // THIS HAS TO MATCH THE AUTHORIZER TYPE ABOVE
    });
```

Basically loop through the routes and tell cloudformation that each of these should use the authorizer we created.

![Alt Text](https://media.giphy.com/media/W9lzJDwciz6bS/giphy.gif)

# Let's try it <a name="try"></a>

First we'll start without a token... we should get an unauthorized message:

```bash
$ curl --url https://9gegpiidnb.execute-api.us-east-1.amazonaws.com/
{"message":"Unauthorized"}%                                                                                                                                                      
```

Great!  _Note:  I've already destroyed the app when writing this... so that specific endpoint won't work for you no matter what_

Now, let's go to Auth0... go to our M2M application and go to the quickstart tab.  It looks like this:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/naz1u0k66wby63dohaao.png)

Grab the cURL request to get an access token and use it...

```bash
$ curl --request POST \
  --url https://martzcodes.us.auth0.com/oauth/token \
  --header 'content-type: application/json' \
  --data '{"client_id":"87ukcfHCe1LGpP7p5PbK8Mrsisnf91xF","client_secret":"WlgVpE3IYja5GkaYcEd8JqIxz0Qx0Oc_MI_V3fad3tyY7","audience":"https://martzcodes.us.auth0.com/api/v2/","grant_type":"client_credentials"}'
{"access_token":"<some.auth0.token>","scope":"read:users","expires_in":86400,"token_type":"Bearer"}%
```

(don't worry security friends... all of that is obfuscated in some way)

With the access token in hand, we can make a request to our API and it _should_ be allowed.

```bash
$ curl --url https://9gegpiidnb.execute-api.us-east-1.amazonaws.com/ \
 --header 'content-type: application/json' \
 --header "Authorization: Bearer <access_token from above>"
yeah it worked% 
```

![Alt Text](https://media.giphy.com/media/mSXSC0vivvygw/giphy.gif)

But what does the `event` inside of the lambda look like?

If I check the cloudwatch logs, first I'll notice that the lambda never got invoked when I didn't pass in the correct access token.  I could invoke that all day / every day and still nothing would get invoked.  The next thing to notice is the event now contains an authorizer object under `requestContext.authorizer.jwt`... the payload of which looks like this:

```json
{
   ... other stuff ....
    "requestContext": {
        ... other stuff ...
        "authorizer": {
            "jwt": {
                "claims": {
                    "aud": "https://martzcodes.us.auth0.com/api/v2/",
                    "azp": "87ukcfHCe1LGp5PbK8Mrsisnf91xF",
                    "exp": "1596771153",
                    "gty": "client-credentials",
                    "iat": "1596684753",
                    "iss": "https://martzcodes.us.auth0.com/",
                    "sub": "87ukcfHCe1LGpP7p5PbK8Mrsisnf91xF@clients"
                },
                "scopes": null
            }
        },
    ... other stuff ...
}
```

If you have Auth0 set up to add information to the claims via rules, that would go there.  If we had set up scopes on the M2M application, those would also go in there.  Very useful!

# One last thing <a name="gotcha"></a>

There is one glaring issue with the current implementation of the APIGatewayV2 Authorizer setup... You can't use custom authorizer lambda functions!

![Alt Text](https://media.giphy.com/media/saD01LsKDzxL2/giphy.gif)

[The docs seems to support it...](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-authorizer.html) but if you actually try to use a `REQUEST` authorizer type instead of a `JWT` you'll get this message: `Only JWT authorizer type is supported on HTTP protocol Apis`... which, in CDK at least, API Gateway V2 only supports the HttpAPI... there isn't anything else to use.