Secure API Gateway with Cognito

Get it running in under 5 minutes

Alan Zhao
4 min readJul 10, 2023

There are a lot of great articles on this topic and the fundamentals already so I won’t be diving into details. Let’s get straight into the working demo using CloudFormation.

Let’s look at the architecture diagram first.

User logs into Cognito and gets an access token. User then passes the access token in the authorization header to API Gateway. The API Gateway’s JWT authorizer checks the access token and gives access to the Lambda function in the private subnet.

Simple, right? Yes, only if you can provision everything with CloudFormation or any other IaC tool.

Prerequisites

  • A domain name in Route53 and SSL certificate in ACM. OK, why? Well, in production, you don’t really want to expose the Cognito raw URL, hence I am putting both authentication and API URLs behind a custom domain. Just take out the custom domain configurations if you don’t need it.

If your root domain isn’t in use, you have to create an A record and have it point to 8.8.8.8 or 127.0.0.0. This is one of the nuisances setting up custom domain for Cognito.

  • Be prepare to spend some $ on the 2 NAC cost. Hmm..why? I want Lambda to live privately in a subnet so it can consume resources securely.

OAuth Flows

It’s important to understand the OAuth flows provided by Cognito before going further.

Authorization code grant —” code”

Amazon Cognito issues a code to the client. The client can redeem this token at your domain’s token endpoint for access, ID, and refresh tokens. Only authorization code grants can return refresh tokens.

Implicit grant —” implicit”

Amazon Cognito issues access and ID tokens directly to the client. Implicit grants expose tokens directly to the user. You can’t issue refresh tokens for this grant type.

Client credential grant — “client_credentials”

Amazon Cognito issues an access token directly to the client for machine-to-machine token exchange. You must use a client secret, and have a custom scope configured, to use this grant type.

The OAuth flows can be configured in CloudFormation under UserPoolClient::AllowedOAuthFlows. Their values are code, implicit and client_credentials.

Be aware that you can’t use an implicit or code grant type at the same time as a client_credentials grant.

Without further delay, below is the CloudFormation script. Be sure to read through the notes in the script as there are a lot configurations you can customize.

The article continues after the script.

Once the CloudFormation script has successfully run, go to the Output section. Grab either SignupGetTokenURL or SignupGetCodeURL. Both are Cognito hosted UI for signup and login.

In the CloudFormation, UserPool::AutoVerifiedAttributes::email has been deliberately commented out. This mean user can signup with the hosted UI but they can’t verify themself using verification code sent by AWS. I only want admin to approve the users who can access the API. You can alway create the users in the Cognito console.

Proceed with getting access token and passing it onto the API once you have a confirmed user.

Method 1

Use implicit flow and Cognito IDP URL.

#!/bin/bash

API_URL="https://api.your-domain.com"
COGNITO_IDP_URL="https://cognito-idp.us-east-1.amazonaws.com"
CLIENT_ID="XXXXXXXXX"
CLIENT_SECRET="XXXXXXXXX"
USERNAME="youruser"
PASSWORD="yourpassword"

# HMAC Computed hash -- SHA256 with key CLIENT_SECRET of USERNAME+CLIENT_ID and base64'd
# See: https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash
SECRET_HASH=$(echo -n "${USERNAME}${CLIENT_ID}" | openssl dgst -sha256 -hmac ${CLIENT_SECRET} -binary | openssl enc -base64)

# Include SECRET_HASH if app client generates client_secret
response=$(curl -s --location --request POST $COGNITO_IDP_URL \
--header 'X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth' \
--header 'Content-Type: application/x-amz-json-1.1' \
--data-raw "{
\"AuthFlow\": \"USER_PASSWORD_AUTH\",
\"AuthParameters\": {
\"USERNAME\": \"$USERNAME\",
\"PASSWORD\": \"$PASSWORD\",
\"SECRET_HASH\": \"$SECRET_HASH\"
},
\"ClientId\": \"$CLIENT_ID\"
}")

ACCESS_TOKEN=$(echo "$response" | jq -r '.AuthenticationResult.AccessToken')

echo "Access Token: $ACCESS_TOKEN"

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" $API_URL

Method 2

Use implicit flow and Cognito IDP CLI.

#!/bin/bash

API_URL="https://api.your-domain.com"
COGNITO_IDP_URL="https://cognito-idp.us-east-1.amazonaws.com"
CLIENT_ID="XXXXXXXXX"
CLIENT_SECRET="XXXXXXXXX"
USERNAME="youruser"
PASSWORD="yourpassword"
REGION="us-east-1"

# HMAC Computed hash -- SHA256 with key CLIENT_SECRET of USERNAME+CLIENT_ID and base64'd
# See: https://docs.aws.amazon.com/cognito/latest/developerguide/signing-up-users-in-your-app.html#cognito-user-pools-computing-secret-hash
SECRET_HASH=$(echo -n "${USERNAME}${CLIENT_ID}" | openssl dgst -sha256 -hmac ${CLIENT_SECRET} -binary | openssl enc -base64)

# Include SECRET_HASH if app client generates client_secret
response=$(aws cognito-idp initiate-auth \
--auth-flow USER_PASSWORD_AUTH \
--client-id "$CLIENT_ID" \
--auth-parameters "USERNAME=$USERNAME,PASSWORD=$PASSWORD,SECRET_HASH=$SECRET_HASH" \
--region "$REGION" \
--output json \
)

ACCESS_TOKEN=$(echo "$response" | jq -r '.AuthenticationResult.AccessToken')
REFRESH_TOKEN=$(echo "$response" | jq -r '.AuthenticationResult.RefreshToken')

echo "Access Token: $ACCESS_TOKEN"

curl -s -H "Authorization: Bearer $ACCESS_TOKEN" $API_URL

If everything goes well, you should be seeing Hello, World! I am xxxx! in the command output.

Of course there are many other ways to obtain the access token. Hopefully you can at least get a demo running with minimal effort and start playing with it.

--

--