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
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...
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...
Choose the already-existing API (the example one Auth0 created for you)...
The scopes don't really matter for this demo... you can skip those and click "Authorize".
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!
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.
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:
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%
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!
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.
Top comments (2)
IMHO there is a better future with JWT tokens. You're right that it's a v2 feature. Does this mean we must write our own code? AWS Amplify simplified API Gateway v1 which means we are possibly on our own with v2. 😑
Awesome! Just what I was looking for! I'll try :) Thank you!