Add a Cognito Authorizer to API Gateway V2 in AWS CDK

avatar

Borislav Hadzhiev

Last updated: Apr 12, 2022

banner

Check out my new book

Adding Cognito Authorizers to an API in AWS CDK #

In order to control access to our lambda functions, we can make use of authorizers.

In this article we will look at a complete example of how we can protect our Lambda functions with an API Gateway (Cognito JWT) authorizer in a CDK provisioned application.

In order to attach a Cognito Authorizer to an API we have to create the authorizer, by using the HttpUserPoolAuthorizer construct and set the authorizer when creating the API route.

The code for this article is available on GitHub

Project Setup #

The code in the GitHub repository provisions:

  • an API Gateway
  • a Lambda function that only allows authorized user access
  • Cognito User pool and User pool client
  1. Clone the Github Repository
  2. Install the dependencies:
shell
npm install
  1. Create the CDK stack
shell
npx aws-cdk deploy \ --outputs-file ./cdk-outputs.json

Creating Cognito Authorizers for an API using AWS CDK #

Let's focus on the code that's related to provisioning the Cognito Authorizer and attaching it to the API Route:

The code for this article is available on GitHub
lib/cdk-starter-stack.ts
// 👇 create the lambda that sits behind the authorizer const lambdaFunction = new NodejsFunction(this, 'my-function', { runtime: lambda.Runtime.NODEJS_14_X, handler: 'main', entry: path.join(__dirname, `/../src/protected-function/index.ts`), }); // 👇 create the API const httpApi = new apiGateway.HttpApi(this, 'api', { apiName: `my-api`, }); // 👇 create the Authorizer const authorizer = new apiGatewayAuthorizers.HttpUserPoolAuthorizer( 'user-pool-authorizer', userPool, { userPoolClients: [userPoolClient], identitySource: ['$request.header.Authorization'], }, ); // 👇 set the Authorizer on the Route httpApi.addRoutes({ integration: new apiGatewayIntegrations.HttpLambdaIntegration( 'protected-fn-integration', lambdaFunction, ), path: '/protected', authorizer, });
If you still use CDK version 1, switch to the cdk-v1 branch in the GitHub repository.

Let's go over what we did in the code snippet:

  1. We created a basic lambda function and an API
  2. We created an authorizer, where we provided the following parameters:
  • userPool - the user pool this authorizer will be associated with
  • userPoolClients - an array containing the user pool client(s) that will be used to authorize requests with the user pool
  • identitySource - the identity source, which requests authorization. In our case the JWT token will be passed in the Authorization HTTP header to authorize a user
  1. We added a route to the API and set the authorizer property to the authorizer we created
The code for this article is available on GitHub

The code for the Lambda function that only allows for authorized access could be as simple as:

src/protected-function/index.ts
import {APIGatewayProxyEventV2, APIGatewayProxyResultV2} from 'aws-lambda'; export async function main( event: APIGatewayProxyEventV2, ): Promise<APIGatewayProxyResultV2> { console.log('This function can only be invoked by authorized users'); console.log('event', JSON.stringify(event, null, 2)); return {body: JSON.stringify({message: 'SUCCESS'}), statusCode: 200}; }

The function will only get invoked after the Authorizer has checked for the validity of the JWT token that was provided in the Authorization HTTP header.

Testing the Cognito JWT Authorizer #

Let's test if our lambda function is protected by the authorizer.

In order to test the flow we have to:

  1. Create a Cognito User
  2. Confirm the user, so they can sign in
  3. Log the user in to get an identity JWT token
  4. Use the token to invoke our API endpoint which will call the function (if the token is valid).
You can find the userPoolId, userPoolClientId and apiUrl identifiers in the cdk-outputs.json file in the root directory of your project. Alternatively you can grab them using the AWS Console.
Make sure you don't confuse the User Pool id and the User Pool Client id, because the commands below use both.
  1. Register a user, using the aws cli
shell
aws cognito-idp sign-up \ --client-id YOUR_USER_POOL_CLIENT_ID \ --username "test@test.com" \ --password "password123"
  1. Confirm the user, so they can log in
shell
aws cognito-idp admin-confirm-sign-up \ --user-pool-id YOUR_USER_POOL_ID \ --username "test@test.com"

At this point if you look at your cognito user pool, you would see that the user is confirmed and ready to log in:

confirmed user

  1. Log the user in
shell
aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters \ USERNAME="test@test.com",PASSWORD="password123" \ --client-id YOUR_USER_POOL_CLIENT_ID

You will get a very verbose response due to the length of these tokens. We only care about the IdToken, so copy and paste it into a notepad because we will need it when invoking the API.

  1. Hit our Api to test the Authorizer. Note: you can find the Api Url in the cdk-outputs.json file in the root directory, or by opening the API gateway service in the AWS Console.

First we will send an anonymous request, without providing the Authorization header. The expected behavior would be to get a 401 Unauthorized response:

shell
curl --location --request GET 'YOUR_API_URL/protected'

The response is:

api-response
{"message": "Unauthorized"}

At this point we know that our Api authorizer works, let's try to send along the Authorization header and test the happy path:

shell
curl --location --request GET 'YOUR_API_URL/protected' \ --header 'Authorization: YOUR_ID_TOKEN'

The response comes straight from our Lambda function:

api-response
{"message": "SUCCESS"}

At this point we know that our authorizer works and validates the Authorization header.

Cleanup #

To delete the provisioned resources, run the destroy command:

shell
npx aws-cdk destroy

Discussion #

Cognito authorizers enable us to place our lambda functions behind API Gateway, which checks for the validity of the user's JWT token provided in the Authorization header.

Api authorizers can be of 3 types:

  • Lambda authorizers - you can provision a lambda function and based on the event, permit/forbid a request to go through. However, you have to write the logic yourself.
  • JWT authorizers - based on a JWT token's validity (most commonly passed in the Authorization http header), the authorizer automatically permits / stops a request to your lambda function.
  • Standard AWS IAM roles and policies - they allow you to create custom roles and policies to control who can call your API.

In this article we used a JWT authorizer as it is the most intuitive and common way to protect lambda functions.

Further Reading #

I wrote a book in which I share everything I know about how to become a better, more efficient programmer.
book cover
You can use the search field on my Home Page to filter through all of my articles.