Securing access to AWS API Gateway using Amazon Cognito User Pools as an authorizer

What is AWS API Gateway

Amazon API Gateway is a fully managed service that enables the developers to create, publish, maintain, monitor, and secure APIs at the desired scale. APIs act as the “entry point” for applications to access data, business logic, feature or functionality from your backend services. API Gateway enables you to create RESTful APIs and WebSocket APIs that allow real-time two-way communication applications. API Gateway supports containerized and serverless, as well as web applications.

Why is API security important?

Why should you care about API security? The purpose of APIs is to connect services and transfer data. APIs that are exploited, or hacked lead to critical data breaches. Personal and even financial data is exposed. API security isn’t taken as seriously as it should be considering how damaging an API security breach can be for a person or business.

What are some of the most common API security best practices?

Here are some of the most common ways you can strengthen your API security:

  • Use tokens
  • Use encryption and signatures
  • Use quotas and throttling
  • Use an API gateway

What is Amazon Cognito?

Amazon Cognito lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily. Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0.

What is Cognito User Pools?

A user pool is a user directory in Amazon Cognito. With a user pool, your users can sign in to your web or mobile app through Amazon Cognito. Your users can also sign in through social identity providers like Google, Facebook, Amazon, or Apple, and through SAML identity providers. Whether your users sign in directly or through a third party, all members of the user pool have a directory profile that you can access through a Software Development Kit (SDK).

Setting up the User Pool

Log in to the AWS Console and go to Cognito section

  1. Give a name to your Pool and click on Step through settings
Cognito | Create a user pool

2. Go through each step and change any setting if required as per your requirement. Under the App clients step you can click on the Add an app client link and create an app client as mentioned below.

Cognito | Which app client will have access to this user pool?
Cognito | Which app client will have access to this user pool?
Cognito | Which app client will have access to this user pool?

Integrating API Gateway with Cognito User Pools as an Authorizer

1. Go to the Services section in your AWS Console and select API Gateway. Click on Build under REST API

API Gateway | REST API

2. Enter the API name and Description of your choice and click Create API

API Gateway | Create new API

3. Once the API is created, click on the Authorizers link from the side menu

API Gateway | Authorizers

4. Click on Create New Authorizer and select the Cognito user pool and pass Token source as Authorization

API Gateway | Authorizers
API Gateway | Authorizers

5. Now create a method under the newly created API

API Gateway | Method

6. Once the method is created click on the method to add auth to the API

API Gateway | Add Auth to API

7. Click on the method request and select the authorizer which you have created in the previously

API Gateway | Add Auth to API using Cognito User Pools
API Gateway | Add Auth to API using Cognito User Pools

Create user-sign-in API and Lambda function

1. Create Lambda function for user-sign-in choose Runtime as Python 3.8 and create the function with Cognito permissions.

IAM Role:

{
"Version": "2012-10-17",
"Statement": [
{
"Action": "logs:CreateLogGroup",
"Resource": "*",
"Effect": "Allow"
},
{
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": [
"*"
],
"Effect": "Allow"
},
{
"Action": "cognito-idp:AdminInitiateAuth",
"Resource": "<resource-arn>",
"Effect": "Allow"
}
]
}

Lambda Function:

import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import json
import os
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
USER_POOL_ID = os.environ['USER_POOL_ID']
CLIENT_ID = os.environ['CLIENT_ID']
CLIENT_SECRET = os.environ['CLIENT_SECRET']
response = {}
response['status'] = ""
response['code'] = ""
response['data'] = {}
response['message'] = ""
def get_secret_hash(username):
msg = username + CLIENT_ID
dig = hmac.new(str(CLIENT_SECRET).encode('utf-8'),
msg=str(msg).encode('utf-8'), digestmod=hashlib.sha256).digest()
d2 = base64.b64encode(dig).decode()
return d2
def initiate_auth(client, username, password):
secret_hash = get_secret_hash(username)
try:
resp = client.admin_initiate_auth(
UserPoolId=USER_POOL_ID,
ClientId=CLIENT_ID,
AuthFlow='ADMIN_NO_SRP_AUTH',
AuthParameters={
'USERNAME': username,
'SECRET_HASH': secret_hash,
'PASSWORD': password,
},
ClientMetadata={
'username': username,
'password': password,
})
except client.exceptions.NotAuthorizedException:
return None, "The username or password is incorrect"
except client.exceptions.UserNotConfirmedException:
return None, "User is not confirmed"
except Exception as e:
return None, e.__str__()
return resp, None
def lambda_handler(event, context):
client = boto3.client('cognito-idp')
for field in ["username", "password"]:
if event.get(field) is None:
response['status'] = "failure"
response['code'] = "10022"
response['message'] = f"{field} is required"
return response
username = event['username']
password = event['password']
resp, msg = initiate_auth(client, username, password)
if msg != None:
response['status'] = "failure"
response['code'] = "10023"
response['message'] = msg
return response
if resp.get("AuthenticationResult"):
response['status'] = "success"
response['code'] = "10024"
response['message'] = msg
response['data'] = {
"id_token": resp["AuthenticationResult"]["IdToken"],
"refresh_token": resp["AuthenticationResult"]["RefreshToken"],
"access_token": resp["AuthenticationResult"]["AccessToken"],
"expires_in": resp["AuthenticationResult"]["ExpiresIn"],
"token_type": resp["AuthenticationResult"]["TokenType"]
}
return response
else:
response['status'] = "failure"
response['code'] = "10025"
response['message'] = "Something went wrong! Please try again."
return response

Lambda Response

{
"status": "success",
"code": "10024",
"data": {
"id_token": "<id-token>",
"refresh_token": "<refresh-token>",
"access_token": "<access-token>",
"expires_in": 3600,
"token_type": "Bearer"
},
"message": null
}

2. API Gateway — Lambda Integration

API Gateway | user-sign-in lambda integration

Testing the API (my-api) with an Authorization header

You need to pass the id_token from the user-sign-in API response as an Authorization header to the my-api

  1. Use Postman to test the API and try to hit the API without passing the Authorization header. It gives an Unauthorized error message.
Unauthorized request

2. Now hit the same API with the Authorization header. It will give the response as expected.

Successful request

If you have any questions, please feel free to connect with me on LinkedIn

If you find this article helpful please feel free to clap!
Cheers!!

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store