Part 1 : Securing AWS API Gateway using AWS Cognito OAuth2 scopes

karthik
10 min readApr 17, 2018

--

In the previous blog, we saw how to secure API Gateway using custom authorizer which talks to OpenAM. In this blog, we are going to see how to secure API Gateway using AWS Cognito and OAuth2 scopes.

Use Case :

Any organization building an API based architecture has to build a common security layer around these APIs, basically on the edge so that all the APIs are secured. There are multiple ways to build API security like writing some filters in the case of Java / J2EE application, installing some agents in front of APIs which can make policy decisions etc. One of the most widely used protocol for Authorization is OAuth2. AWS API Gateway provides built-in support to secure APIs using AWS Cognito OAuth2 scopes.

Below is the architecture diagram:

  1. Invoke AWS Cognito /oauth2/token endpoint with grant_type as client_credentials. Refer https://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
  2. If the request is valid, AWS Cognito will return a JWT (JSON Web Token) formatted access_token
  3. Pass this token in Authorization header for all API calls
  4. API Gateway makes a call to AWS Cognito to validate the access_token.
  5. AWS Cognito returns token validation response.
  6. If token is valid, API Gateway will validate the OAuth2 scope in the JWT token and ALLOW or DENY API call. This is entirely handled by API Gateway once configuration is in place
  7. Perform the actual API call whether it is a Lambda function or custom web service application.
  8. Return the results from Lambda function.
  9. Return results to API Gateway.
  10. If there are no issues with the Lambda function, API Gateway will return a HTTP 200 with response data to the client application.

There are few prerequisites for setting up this integration:

  1. AWS Account — business or free tier.
  2. Knowledge on AWS API Gateway , AWS Cognito services
  3. Knowledge on OAuth2 protocol

We have to perform the below steps for this integration:

  1. Create a AWS Cognito user pool and configure OAuth agents
  2. Deploy a sample micro webservice application using AWS API Gateway and Lambda
  3. Configure Cognito Authorizer in API Gateway

If you would like to understand how OAuth2 client credentials work, you can watch the below video. If you are already aware of how it works, move to the next section :

AWS Setup — Step-by-Step guide

In order to make things easier, I published a youtube video on how to configure this setup in AWS. Alternatively, you can follow the detailed steps in the blog.

Also, there were lots of questions on how to setup a Cognito IDP for user authentication and generate ID, Access tokens. Below video explains the steps to setup Cognito IDP for user authentication. Basically, the access tokens generated after user authentication can be used to access APIs.

If you are interested in how to secure a AWS ALB using OIDC provider like Cognito / Okta / Any OIDC provider, you can follow the step-by-step instructions in these video series :

Step 1: Create AWS Cognito user pool and setup a OAuth application

  • Login to AWS Management console and navigate to Cognito service
  • Select “Manage your user pools” and click “Create a user pool”
  • Enter a pool name and select “Review defaults”. Then select “Create pool”.
  • Navigate to “General Settings > App clients” and select “Add an app client”
  • Enter a “App client name” and select “Generate client secret” checkbox. Then “Create app client”. Note down the “App client id” and “App client secret” values displayed in next page.
  • Go to “Domain name” and enter your own domain name. It can be any name like test, test123 etc. You can check if the domain is available or not. Let us assume that domain name is api-product. So, the URL would be https://api-product.auth.us-east-1.amazoncognito.com
  • Go to “Resource Servers” and “Add a resource server”
  • Enter “Name” and “Identifier”. It can be any value. Also, add 3 scopes and save changes.
  1. read_product
  2. create_product
  3. delete_product
  • Go to “App client settings” and you should see the configuration page for new App client. For “Enabled Identity providers” , select “Cognito User pool” checkbox. Then select “Client credentials” checkbox for “Allowed OAuth flows”. Select all the scopes for “Allowed custom scopes” and save changes. If certain clients should have only “read_product” scope, then select only that checkbox.
  • Now, we have successfully setup a OAuth2 agent in Cognito. Below CURL command should return an access token
//Replace app client id and secret accordingly. Also, the URL will change if you had selected a different domain namecurl -X POST --user <app client id>:<app client secret> 'https://api-product.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials' -H 'Content-Type: application/x-www-form-urlencoded'{"access_token":"eyJraWQiOiJFTUlrM3NBSjhUQ0s3a0l4UTdzQ1dJTmEyeW5OTW93bzcxVDlYU2VoN3pjPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI0czRjN3FtdGtvYTJrZms0Ym0wZXNnazMzYyIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoicHJvZHVjdC1hcGlcL2RlbGV0ZV9wcm9kdWN0IHByb2R1Y3QtYXBpXC9yZWFkX3Byb2R1Y3QgcHJvZHVjdC1hcGlcL2NyZWF0ZV9wcm9kdWN0IiwiYXV0aF90aW1lIjoxNTIzOTE2Nzc1LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV8yRVhzSENwR3QiLCJleHAiOjE1MjM5MjAzNzUsImlhdCI6MTUyMzkxNjc3NSwidmVyc2lvbiI6MiwianRpIjoiZDhlZTkwMGItOTFhZC00ZjVmLTk0YmItN2Q2NTcwYTNlMWQzIiwiY2xpZW50X2lkIjoiNHM0YzdxbXRrb2Eya2ZrNGJtMGVzZ2szM2MifQ.bMup2dXwteG8-lVTQcoO-lBuaPLYc7DONNAVW78peOi3KZzM9T6ZmKLKkjW8WIfk4Cq2ZNMrDchWvZoscZ848Kvd6M7aYUQtjfVRHyWuIVDCIUhyIUMmVJUmo9mh78Qq13u5Rsvbs5V1nezRRB0qIqr4SNGUsfV-lfAWXVYZtZYPU7xiVjjycAffDCgqjq91WfpX2AsvooaqiYgYKQ-5bzPj41kWf3ogLl18DOV4w5bHWhD8BjWHmA8H4R2O_039ecwcv0itz4S2-YlqzIX9NNtdL8DT7Wf8h8y-rJrUYj0iE1FyEisg9L1FoLpaMlR7Mf8ksdne8J05kx7vp7YwPQ","expires_in":3600,"token_type":"Bearer"}
  • You can copy and paste the access_token value in https://jwt.io/ website. You should see all the 3 scopes in the token by default — “scope”: “product-api/delete_product product-api/read_product product-api/create_product”
  • Cognito follows the OAuth2 specification. You can try passing a specific scope in the CURL command and check the token.
//access_token returned with below CURL command should return only product-api/delete_product scopecurl -X POST --user <app client id>:<app client secret> 'https://api-product.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials&scope=product-api/delete_product' -H 'Content-Type: application/x-www-form-urlencoded'

Step 2: Setup a sample micro service application in AWS using API Gateway and Lambda

It is pretty easy to setup the sample micro service application.

  • Login to AWS Management console and navigate to AWS Lambda service.
  • Create a new function. In this screen, select Blueprints and search for microservice
  • Select microservice-http-endpoint-python and click configure.
  • In Basic Information screen, enter a Name for this function, Role name and leave other fields as default. For this example, name it as “OAuthAPITest”
  • In api-gateway section, select “Create a new API”, provide a API name, Deployment stage and select “Open” for Security.
  • scroll down and click “Create function”. This should automatically create Lambda function, corresponding API Gateway endpoints.
Lambda Function
API Gateway
  • Click “API_Cognito” link and select “Stages” in left navigation bar. Select “dev” stage and copy the “Invoke URL” value.
  • Since this service requires a Dynamo DB instance, navigate to DynamoDB service and create a new table called “products” with primary key “id”.
  • Open a REST client like postman or terminal and create 2 records in dynamo DB table using below CURL command
//Replace the <Invoke URL> value of API Gatewaycurl -X POST 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -d '{"TableName": "products","Item": {"product": {"S": "android"},"id": {"S": "1"}}}'curl -X POST 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -d '{"TableName": "products","Item": {"product": {"S": "apple"},"id": {"S": "2"}}}'
  • Execute below CURL command to test GET
curl -X GET 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json'
  • Execute below CURL command to test DELETE
//Change the "id" value accordingly to delete both productscurl -X DELETE 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -d '{"TableName": "products","Key": {"id":{"S": "1"}}}'
  • Let us modify the API methods. Navigate to “Resources” in API_Cognito configuration. You will see only one method “ANY” below “/OAuthAPITest” which means it accepts all HTTP methods.
  • Select “ANY” and then “Actions > Delete Method”
  • Select “/OAuthAPITest” and then “Actions > Create Method”. Select “GET” and click the tick mark to add it. In the Setup screen, select “Use Lambda Proxy Integration” checkbox and enter “OAuthAPITest” for Lambda Function. On clicking “Save”, it will show a prompt for permissions and click OK.
  • This should create the GET method. Similarly, create 2 more methods “POST” and “DELETE” with same configuration. You should see 3 methods after this.
  • Now, select “Actions > Deploy API” and select “dev” for “Deployment Stage”. Click “Deploy”.
  • Wait for couple of minutes since it will take sometime to deploy the changes. Then test the POST, GET and DELETE CURL commands again. It should work.

Step 3: Configure Cognito Authorizer for API Gateway

  • Go to “Amazon API Gateway > API_Cognito > Authorizers” and “Create new Authorizer”. Enter a Name and select user pool which was created in Step 1. Also, enter “Token Source” as “Authorization” header.
  • Go to “Resources” and select “GET” method. Select “Method Request” configuration on right pane.
  • Select “Cognito_Authorizer” in “Authorization” drop-down. That should automatically add a new field “OAuth Scopes”. Enter “product-api/read_product” scope and save using the tick mark.
  • Similarly, map DELETE method to “product-api/delete_product” and POST method to “product-api/create_product”
  • Select “Actions > Deploy API” and select “Deployment stage” as “dev”. This should deploy the latest changes in these APIs.

Step 4: Testing

Now, let us test this API using the access_token obtained from Cognito

  • Get access_token from Cognito
//Change app client id, secret and URL accordinglycurl -X POST --user <app client id>:<app client secret> 'https://api-product.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials' -H 'Content-Type: application/x-www-form-urlencoded'
  • If you try the previous CURL POST request to create a product in DynamoDB, it won’t work because API Gateway expects a Authorization header with access_token. Also, the token should contain the scope product-api/create_product
//Replace the <Invoke URL> and access_token accordingly. If there is any issue with the token, API Gateway will return 401 error.curl -X POST 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -H 'Authorization:<access_token>' -d '{"TableName": "products","Item": {"product": {"S": "apple"},"id": {"S": "2"}}}'
  • Execute the below CURL command to get the product details
curl -X GET 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -H 'Authorization:<access_token>'
  • Execute the below CURL command to delete the product details
curl -X DELETE 'https://<Invoke URL>/OAuthAPITest?TableName=products' -H 'Content-Type: application/json' -H 'Authorization:<access_token>' -d '{"TableName": "products","Key": {   "id":{"S": "1"}}}'
  • To make sure scope based Authorization is working fine, make /token API call with a specific scope, say scope=product-api/create_product. If this access_token is used for GET / DELETE API calls, API Gateway will return “Unauthorized” error.
curl -X POST --user <app client id>:<app client secret> 'https://api-product.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=client_credentials&scope=product-api/create_product' -H 'Content-Type: application/x-www-form-urlencoded'

This concludes the setup. By creating multiple clients with different scopes, API access can be controlled per client application. Please note that in this use case, we used client_credentials grant which is not user specific, but application specific. To make it user specific, we have to use OpenID Connect which will be a totally different configuration. Please refer this blog on how to implement the same use case using OpenID Connect.

There are multiple ways to control access to AWS services. AWS Cognito’s Role-Based Access Control can be used to control access to not just API Gateway, but to all AWS services. Refer this blog.

Other blogs that might be of interest :

Thanks for reading this article. Please subscribe to the below YouTube channel and follow me in medium to learn about security and IAM.

--

--