In this tutorial, we are going to see how to secure API gateway endpoints using CognitoUserPool.

The user pool is a user directory in Amazon Cognito using which users can sign in to the web or mobile applications through Amazon Cognito. The Amazon UserPool supports different identity providers such as Web federated identity (Facebook, Google, Apple, Amazon, SAML and OpenID connect) and CognitoUserPool.

Securing API Gateway endpoints using CognitoUserPool:

As part of this tutorial, we are going to secure API gateway endpoints using CognitoUserPool Identity provider Client credentials.

Technologies Used:

  • Python 3.8
  • AWS SAM
  • SAM CLI
  • Poetry

Following are the AWS services which we are going to create using SAM

  • AWS Cognito
    • Cognito UserPool
    • App Client
    • Cognito Domain
    • Resource Server
  • API Gateway
    • Rest API endpoint (/categories)
    • CognitoAuthorizer
  • Lambda
    • Categories Lambda
    • Layers

UserPool:

Creating Cognito UserPool

template.yaml
Resource:
  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub ${Stage}-Cognito-User-Pool

UserPoolClient:

Creating COGNITO UserPoolClient which enables the OAuthFlowwith client_credentialsand allowing OAuthScopes for all CRUD endpoints.

template.yaml
Resource:
  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: CognitoDomainNameResourceServer
    Properties:
      UserPoolId: !Ref CognitoUserPool
      ClientName: !Sub ${Stage}-CognitoUserPoolClient
      GenerateSecret: true
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthFlows:
        - client_credentials
      SupportedIdentityProviders:
        - COGNITO
      AllowedOAuthScopes:
        - access_points/read
        - access_points/delete
        - access_points/update
        - access_points/write

CognitoDomain:

Creating UserPoolDomain and mapped with CongitoUserPool

template.yaml
Resource:
  CognitoDomainName:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Sub ${Stage}-domain
      UserPoolId: !Ref CognitoUserPool

Resource Server:

Creating a UserpoolResourceServerwith all CRUD (create, read, update, delete) scopes.

template.yaml
Resource:
  CognitoDomainNameResourceServer:
    Type: AWS::Cognito::UserPoolResourceServer
    Properties:
      Identifier: access_points
      Name: !Sub ${Stage}-resource-server
      Scopes:
        - ScopeDescription: "Read Resources"
          ScopeName: "read"
        - ScopeDescription: "Write Resources"
          ScopeName: "write"
        - ScopeDescription: "Delete Resources"
          ScopeName: "delete"
        - ScopeDescription: "Update Resources"
          ScopeName: "update"
      UserPoolId: !Ref CognitoUserPool

API-Gateway:

Creating API-Gateway with CustomCognitoAuthorizerand read,writeauthorization scopes.

template.yaml
Resource:
  CognitoPlatformApi:
    Type: AWS::Serverless::Api
    DependsOn: CognitoUserPoolClient
    Properties:
      Name: !Sub "${Stage}-Cognito-Platform-Api-Gateway"
      StageName: !Ref Stage
      Auth:
        DefaultAuthorizer: CustomCognitoAuthorizer
        Authorizers:
          CustomCognitoAuthorizer:
            UserPoolArn: !GetAtt CognitoUserPool.Arn
            AuthorizationScopes:
              - access_points/read
              - access_points/write

Lambda Dependency Layer:

template.yaml
Resources:
  PythonDepLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub "${Stage}-cognito-serverless-platform-dep-layer"
      CompatibleRuntimes:
        - python3.8
      ContentUri: ./.build/dependencies
      RetentionPolicy: Delete

Categories Lambda:

Lambda function mapped with categories.py

template.yaml
Resources:
  CategoriesLambda:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Stage}-categories-function
      Description: !Sub ${Stage}-categories lambda function
      Handler: src.categories.handle
      Role: !GetAtt CognitoPlatformLambdaRole.Arn
      Events:
        CategoriesApi:
          Type: Api
          Properties:
            RestApiId: !Ref CognitoPlatformApi
            Path: /categories
            Method: GET

Complete SAM template:

template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
  Cognito Serverless Platform AWS Backend
Parameters:
  Stage:
    Type: String
    Default: dev

Globals:
  Function:
    Timeout: 300
    Runtime: python3.8
    Layers:
      - !Ref PythonDepLayer
    CodeUri: ./cognito_serverless_platform
    Tracing: Active
    Environment:
      Variables:
        STAGE: !Ref Stage

Resources:
  PythonDepLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: !Sub "${Stage}-cognito-serverless-platform-dep-layer"
      CompatibleRuntimes:
        - python3.8
      ContentUri: ./.build/dependencies
      RetentionPolicy: Delete

  CategoriesLambda:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Stage}-categories-function
      Description: !Sub ${Stage}-categories lambda function
      Handler: src.categories.handle
      Role: !GetAtt CognitoPlatformLambdaRole.Arn
      Events:
        CategoriesApi:
          Type: Api
          Properties:
            RestApiId: !Ref CognitoPlatformApi
            Path: /categories
            Method: GET

  CognitoPlatformLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole'
        - 'arn:aws:iam::aws:policy/AWSLambdaExecute'
        - 'arn:aws:iam::aws:policy/AmazonCognitoPowerUser'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - 'lambda.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Policies:
        - PolicyName: 'SecretsManagerParameterAccess'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ssm:GetParam*
                  - ssm:DescribeParam*
                  - kms:GetSecretValue
                  - kms:Decrypt
                Resource:
                  - arn:aws:ssm:*:*:parameter/*

  CognitoPlatformApi:
    Type: AWS::Serverless::Api
    DependsOn: CognitoUserPoolClient
    Properties:
      Name: !Sub "${Stage}-Cognito-Platform-Api-Gateway"
      StageName: !Ref Stage
      Auth:
        DefaultAuthorizer: CustomCognitoAuthorizer
        Authorizers:
          CustomCognitoAuthorizer:
            UserPoolArn: !GetAtt CognitoUserPool.Arn
            AuthorizationScopes:
              - access_points/read
              - access_points/write

  CognitoUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub ${Stage}-Cognito-User-Pool

  CognitoDomainNameResourceServer:
    Type: AWS::Cognito::UserPoolResourceServer
    Properties:
      Identifier: access_points
      Name: !Sub ${Stage}-resource-server
      Scopes:
        - ScopeDescription: "Read Resources"
          ScopeName: "read"
        - ScopeDescription: "Write Resources"
          ScopeName: "write"
        - ScopeDescription: "Delete Resources"
          ScopeName: "delete"
        - ScopeDescription: "Update Resources"
          ScopeName: "update"
      UserPoolId: !Ref CognitoUserPool

  CognitoUserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    DependsOn: CognitoDomainNameResourceServer
    Properties:
      UserPoolId: !Ref CognitoUserPool
      ClientName: !Sub ${Stage}-CognitoUserPoolClient
      GenerateSecret: true
      AllowedOAuthFlowsUserPoolClient: true
      AllowedOAuthFlows:
        - client_credentials
      SupportedIdentityProviders:
        - COGNITO
      AllowedOAuthScopes:
        - access_points/read
        - access_points/delete
        - access_points/update
        - access_points/write

  CognitoDomainName:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Sub ${Stage}-domain
      UserPoolId: !Ref CognitoUserPool

Outputs:
  CognitoPlatformApi:
    Description: 'API Gateway endpoint URL'
    Value: !Sub 'https://${CognitoPlatformApi}.execute-api.${AWS::Region}.amazonaws.com/${Stage}/'
  CognitoDomainName:
    Description: 'CognitoDomainName URL'
    Value: !Sub 'https://${CognitoDomainName}.auth.${AWS::Region}.amazoncognito.com'
  CognitoPlatformApiRestApiId:
    Description: 'API Gateway ARN for Basic AWS API Gateway'
    Value: !Ref CognitoPlatformApi
    Export:
      Name: !Sub ${Stage}-CognitoPlatformApi-RestApiId
  CognitoPlatformApiRootResourceId:
    Value: !GetAtt CognitoPlatformApi.RootResourceId
    Export:
      Name: !Sub ${Stage}-CognitoPlatformApi-RootResourceId

Build and deploy the application using SAM CLI.

Make sure you installed sam cli on your machine, configure AWS keys and enable virtualenv.

Terminal
(venv) cognito-serverless-platform % sam build  
Building codeuri: /Users/chandra/Work/MyWork/up-work/cognito-serverless-platform/cognito_serverless_platform runtime: python3.8 metadata: {} functions: ['CategoriesLambda']
Running PythonPipBuilder:ResolveDependencies
Running PythonPipBuilder:CopySource

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided

Deploy:

Terminal
(venv) chandra cognito-serverless-platform % sam deploy
2021-09-01 11:33:12,661 | Telemetry endpoint configured to be https://aws-serverless-tools-telemetry.us-west-2.amazonaws.com/metrics
2021-09-01 11:33:12,661 | Using config file: samconfig.toml, config environment: default
2021-09-01 11:33:12,661 | Expand command line arguments to:
2021-09-01 11:33:12,661 | --template_file=/Users/chandra/Work/MyWork/cognito-serverless-platform/.aws-sam/build/template.yaml --stack_name=dev-cognito-serverless-platform --s3_prefix=dev --s3_bucket=cognito-serverless-platform-dev --parameter_overrides={'Stage': 'dev'} --capabilities=('CAPABILITY_AUTO_EXPAND', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_IAM') 
...
Waiting for changeset to be created..

CloudFormation stack changeset
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                                             LogicalResourceId                                     ResourceType                                          Replacement                                         
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                                 CategoriesLambdaCategoriesApiPermissionStage          AWS::Lambda::Permission                               N/A                                                 
+ Add                                                 CategoriesLambda                                      AWS::Lambda::Function                                 N/A                                                 
+ Add                                                 CognitoDomainNameResourceServer                       AWS::Cognito::UserPoolResourceServer                  N/A                                                 
+ Add                                                 CognitoDomainName                                     AWS::Cognito::UserPoolDomain                          N/A                                                 
+ Add                                                 CognitoPlatformApiDeployment5d8eeb4770                AWS::ApiGateway::Deployment                           N/A                                                 
+ Add                                                 CognitoPlatformApiStage                               AWS::ApiGateway::Stage                                N/A                                                 
+ Add                                                 CognitoPlatformApi                                    AWS::ApiGateway::RestApi                              N/A                                                 
+ Add                                                 CognitoPlatformLambdaRole                             AWS::IAM::Role                                        N/A                                                 
+ Add                                                 CognitoUserPoolClient                                 AWS::Cognito::UserPoolClient                          N/A                                                 
+ Add                                                 CognitoUserPool                                       AWS::Cognito::UserPool                                N/A                                                 
+ Add                                                 PythonDepLayer86256aeadb                              AWS::Lambda::LayerVersion                             N/A                                                 
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:us-west-2:123456:changeSet/samcli-deploy1630476213/9ea40ff2-978d-4c1e-9b04-fc73a774cea7
....
CloudFormation outputs from deployed stack
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                                                                                                                
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 CognitoPlatformApi                                                                                                                                                                                 
Description         API Gateway endpoint URL                                                                                                                                                                           
Value               https://nblbadnr50.execute-api.us-west-2.amazonaws.com/dev/                                                                                                                                        

Key                 CognitoDomainName                                                                                                                                                                                  
Description         CognitoDomainName URL                                                                                                                                                                              
Value               https://dev-domain.auth.us-west-2.amazoncognito.com                                                                                                                                                

Key                 CognitoPlatformApiRootResourceId                                                                                                                                                                   
Description         -                                                                                                                                                                                                  
Value               iba2uhxkfe                                                                                                                                                                                         

Key                 CognitoPlatformApiRestApiId                                                                                                                                                                        
Description         API Gateway ARN for Basic AWS API Gateway                                                                                                                                                          
Value               nblbadnr50                                                                                                                                                                                         
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - dev-cognito-serverless-platform in us-west-2

In the output logs, you can find the API gateway deployment URL and Cognito-domain URL.

Note: The API-gateway URL generated by AWS as we haven’t set up a custom domain for this application,

As a result of the above sam deploy command, we should see the infrastructure in the AWS console.

Head over to AWS console and search for Cognito.

You can find Cognito UserPool dev-Cognito-User-Poolclick on this to view the details.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool

Let’s verify App clients, App client settings and Domain.

Click on App clients in the left navigation pane, as per our SAM template you should see the following settings.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool2

AppClient settings:

Click on AppClient settings in the left navigation pane.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool3

App integration Domain name:

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool4

Cognito ResourceServer:

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool6

API-Gateway:

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool5

You may already notice that the API gateway above was attached with CustomCognitoAuthorizer

As we have seen that the infrastructure was built as expected, now let’s access the API gateway resources.

We have implemented UserPoolClient with allowing OAuth flow client_credentials, so that we need client credentials to generate the Authorization token. You can find the client credentials (App client id and App client secret) on the App Clients page.

Generating Authorization token:

Open postman -> New Tab ->  Select Authorization Tab -> Choose OAuth 2.0 in Type dropdown.

Fill in the following details:

  • Grant Type: Client Credentials
  • Access Token URL – The domain URL is appended with oauth2/token– As per SAM config our access token should be like this – https://dev-domain.auth.us-west-2.amazoncognito.com/oauth2/token
  • Client ID – {App client id} grab this from your App clients page
  • Client Secret – {App client secret} grab this from your App clients page
  • Scope – We have configured for CRUD scopes but for this call, I am limiting it to only readscope.
  • Client Authentication – Send client credentials in the body

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool7

Click on the Get New Access Token button to generate the access token as a result you see the below popup.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool8

Copy the Access Token from the above result and use this Access token for Authorization while calling the API endpoint.

Open a new tab in postman and try to call our /categoriesendpoint using the Access token.

You can find the API gateway deployment URL in SAM deploy logs. Initially lets access the /categoriesendpoint without using Access Token like below.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool10

As we have seen we got Unauthorizederror, as we protected this endpoint with CognitoUserPool.

Now let’s see what happens if we pass Access Token while calling the same request. Access Token has to be provided as part of the request headers with the name called Authorizationlike the following.

AWS SAM - Secure Api Gateway endpoints using CognitoUserPool9

As a result, we got the 200 response with data from backend lambda.

Done!

References:

Happy Learning 🙂