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
Resource:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: !Sub ${Stage}-Cognito-User-Pool
UserPoolClient:
Creating COGNITO UserPoolClient
which enables the OAuthFlow
with client_credentials
and allowing OAuthScopes for all CRUD endpoints.
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
Resource:
CognitoDomainName:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Sub ${Stage}-domain
UserPoolId: !Ref CognitoUserPool
Resource Server:
Creating a UserpoolResourceServer
with all CRUD (create, read, update, delete) scopes.
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 CustomCognitoAuthorizer
and read
,write
authorization scopes.
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:
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
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:
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.
(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:
(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-Pool
click on this to view the details.
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.
AppClient settings:
Click on AppClient settings in the left navigation pane.
App integration Domain name:
Cognito ResourceServer:
API-Gateway:
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
read
scope. - Client Authentication – Send client credentials in the body
Click on the Get New Access Token
button to generate the access token as a result you see the below popup.
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 /categories
endpoint using the Access token.
You can find the API gateway deployment URL in SAM deploy logs. Initially lets access the /categories
endpoint without using Access Token like below.
As we have seen we got Unauthorized
error, 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 Authorization
like the following.
As a result, we got the 200 response with data from backend lambda.
Done!
References:
Happy Learning 🙂