Add a Cognito Authorizer to API Gateway V2 in AWS CDK

avatar
Borislav Hadzhiev

Last updated: Jan 27, 2024
5 min

banner

# Adding Cognito Authorizers to an API in AWS CDK

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

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.

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:

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

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

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 will 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 looks as follows.

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.

We used a JWT authorizer as it is the most intuitive and common way to protect lambda functions.

# Additional Resources

You can learn more about the related topics by checking out the following tutorials:

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.

Copyright ยฉ 2024 Borislav Hadzhiev