Token Authorizers with APIGatewayV2 (tricks APIGWv1 doesn't want you to know)

·

8 min read

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

Choose wisely...

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

$ 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

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

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

Alt Text

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

Alt Text

CDK Setup

The code for this demo is fairly boring. This commit 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:

$ 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:

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:

    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

We're using the CfnAuthorizer

    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 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:

    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

Let's try it

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

$ 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

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

$ 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.

$ 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

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:

{
   ... 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

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

Alt Text

The docs seems to support it... 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.