Control Access to API Gateway Using Amazon Cognito User Pool as Authorizer

Posted on Jan 28, 2019 #apigateway #awscognito #awslambda #python

Amazon API Gateway is an AWS service where we can create, publish, maintain, monitor, and secure REST APIs at any scale. It is one of the key services to build serverless applications or invoke backend services such as AWS Lambda, Amazon Kinesis, or an HTTP endpoint based on message content. It supports multiple mechanisms for controlling access to an API. In this article, I will demonstrate how to use Amazon Cognito user pools to authenticate our REST APIs and this will be a very basic usage with minimal configuration. First, we need to create a Cognito authorizer for our API Gateway and for this, we need to create a Congnito user pool.

Create a Cognito User Pool

From AWS services, select Cognito. To create a new Cognito user pool:

  • Choose Manage User Pools from the first page.
  • Give a pool name and select Step through settings.
  • Under Attributes, select only Username as I will be using only username and password. Cognito User Pool Attributes
  • Leave Policies as it is.
  • From MFA and verifications, select No verification from Which attributes do you want to verify?.
  • Keep Message customizations, Tags and Devices as it is.
  • Under App clients, create a new app client:
    • Give a client name.
    • Select Generate client secret and Enable sign-in API for server-based authentication (ADMIN_NO_SRP_AUTH). Cognito App Clients
    • Then create app client. Here we’ll get a App client id and App client secret after creating the user pool.
  • Skip Triggers for now. I’ll get back to it later.
  • Review everything and create the pool. Keep note of the Pool Id.

Cognito User Pool Settings

Create Lambda Functions

We need to create two Lambda functions. One is for signup and sign in. Another is for automatically confirm created users. Before that, we need to create a role for Lambda so that it can access Cognito.

Create Role for Lambda

Navigate to Identity and Access Management (IAM). Under Roles, create a new Lambda role called LambdaAuthRole and attach a policy called AmazonCognitoPowerUser. This will give full Cognito access to our Lambda functions.

Create Signup and Signin Lambda

Go to Lambda from AWS services and create a new function:

  • Give a name, signInFunction.
  • Select runtime. I will be using Python 3.7 as runtime.
  • Choose an existing role and select LambdaAuthRole.

Now copy and paste the following code in the Lambda function:

import boto3
import botocore.exceptions
import hmac
import hashlib
import base64
import uuid
 

USER_POOL_ID = 'pool_id'
CLIENT_ID = 'app_client_id'
CLIENT_SECRET = 'app_client_secret'

client = None

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

ERROR = 0
SUCCESS = 1
USER_EXISTS = 2
    
def sign_up(username, password):
    try:
        resp = client.sign_up(
            ClientId=CLIENT_ID,
            SecretHash=get_secret_hash(username),
            Username=username,
            Password=password)
        print(resp)
    except client.exceptions.UsernameExistsException as e:
        return USER_EXISTS
    except Exception as e:
        print(e)
        return ERROR
    return SUCCESS
    
def initiate_auth(username, password):
    try:
        resp = client.admin_initiate_auth(
            UserPoolId=USER_POOL_ID,
            ClientId=CLIENT_ID,
            AuthFlow='ADMIN_NO_SRP_AUTH',
            AuthParameters={
                'USERNAME': username,
                'SECRET_HASH': get_secret_hash(username),
                'PASSWORD': password
            },
            ClientMetadata={
                'username': username,
                'password': password
            })
    except client.exceptions.NotAuthorizedException as e:
        return None, "The username or password is incorrect"
    except Exception as e:
        print(e)
        return None, "Unknown error"
    return resp, None

def lambda_handler(event, context):
    global client
    if client == None:
        client = boto3.client('cognito-idp')

    username = event['username']
    password = event['password']
    is_new = "false"
    user_id = str(uuid.uuid4())
    signed_up = sign_up(username, password)
    if signed_up == ERROR:
        return {'status': 'fail', 'msg': 'failed to sign up'}
    if signed_up == SUCCESS:
        is_new = "true"
    
    resp, msg = initiate_auth(username, password)
    if msg != None:
        return {'status': 'fail', 'msg': msg}
    id_token = resp['AuthenticationResult']['IdToken']
    print('id token: ' + id_token)
    return {'status': 'success', 'id_token': id_token, 
        'user_id': user_id, 'is_new': is_new}

Here, replace pool_id, app_client_id and app_client_secret with appropriate values that we got while creating Cognito user pool. Finally, save the Lambda function.

This Lambda function registers user only if no user is already associated with this username in the user pool. And it authenticates if username is already present in the user pool. In both cases, if it validates, then it will return a token.

Create Auto Confirm User Lambda

Create another Lambda function:

  • Give a name, autoConfirmUserFunction.
  • Again, I will be using Python 3.7 as runtime.
  • Again, Choose an existing role and select LambdaAuthRole.

Now copy and paste the following code in the Lambda function and save:

def lambda_handler(event, context):
    event['response'] = {
        'autoConfirmUser': True
    }
    return event

Add Pre Sign-up Trigger for Cognito User Pool

Now that we have created our autoConfirmUserFunction Lambda, we need to attach it as a Pre sign-up trigger in our created Congnito user pool. Navigate to the Cognito user pool that we just created, under Triggers select autoConfirmUserFunction Lambda as Pre sign-up trigger.

Create API in API Gateway

Finally, we need to create a REST API that will invoke our signInFunction Lambda function to signup or sign in users.

Go to API Gateway from AWS services and create a new API:

  • Select protocol REST, Select create new API, enter API name, select Endpoint type and hit create button.
  • Select Create Resource from Actions dropdown and create a new resource by giving resource name and path.
  • Select the newly created resource and then from Actions dropdown, select Create Method. This will be a POST request. So, select POST.
  • In POST Setup select Lambda Function integration. Uncheck Use Lambda Proxy integration. No need to manually select Lambda Region, it will be auto-selected. Select signInFunction as the Lambda Function and hit save. It will ask for permissions.
  • From Actions select Deploy API. Create a new stage and deploy it.

Test the API

Now, we’ll test our API.

API Gateway method test

From POST - Method Execution select TEST. In Request Body enter:

{
    "username": "rakib",
    "password": "123456"
}

Hit Test and it should return success status, is_new true and an id_token.

API Gateway test result

We’ll use this id_token to authenicate other APIs. is_new true means a new user is created in Congnito user pool. We can confirm it by navigating to our created user pool and under Users and groups submenu. Again, if we hit the Test button, in response we’ll see that is_new is false and a new id_token is generated. Change the password and hit Test again. This time we’ll get fail status. This can also be tested in Postman or other tools. The full API URL can be found under Stages menu, select the stage, resource and POST method and we’ll get the full invoke URL.

Create Authorizer for the API

To authenticate our APIs, we need to create a Authorizer and we’ll select this Authorizer in Method Request of other APIs that we want to authenticate.

To create a new Authorizer:

  • Navigate to Authorizers menu.
  • Give a name, select type Cognito. In Cognito User Pool, select our created user pool.
  • Enter Authorization as Token Source. Token will be passed under this header source. Finally, hit create.

API Gateway method execution

To test this, create a new dummy API or use an existing one, go to Method Execution, select Method Request. Then from Settings choose our created Authorizer as Authentication source. Open postman, send appropriate request to that API by passing the token in Authorization header key.

comments powered by Disqus