Part 2: Securing AWS API Gateway using AWS Cognito OAuth2 scopes and OpenID Connect

karthik
9 min readNov 18, 2019

In the previous blog, we saw how to secure APIs using OAuth2 client credentials grant. This OAuth grant is used mainly for machine to machine or app to app API authorization. If the authorization needs to be performed at an user level, we have to use OpenID Connect which adds an identity layer on top of OAuth layer. At a very high level, difference OAuth2 and OIDC w.r.t tokens is that OAuth 2 generates only access token + optional refresh token where as OIDC generates an id token + access token + optional refresh token. ID token contains user attributes which can be used to perform additional authorization checks or trigger multi-factor authentication while invoking an API.

  1. When user logs in, redirect to AWS Cognito Authorization code grant URL with response_typ as code. Refer https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html. In this step, user should login using the Cognito credentials.
  2. If user credentials are valid, AWS Cognito will return a JWT (JSON Web Token) formatted id_token + access_token + refresh_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. Return mock response to API Gateway.
  8. 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 OIDC and OAuth2 protocol

We have to perform the below steps for this integration:

  1. Create a AWS Cognito user pool and configure OAuth agents
  2. Create Cognito user to test the Authorization code grant flow
  3. Deploy a sample API Gateway application with 3 HTTP methods — GET, POST, DELETE and static response
  4. Configure Cognito Authorizer in API Gateway

You can follow the instructions in below video to setup a OIDC Authorization code grant flow using AWS Cognito and test the end-end flow.

Please follow the step by step instructions below this video to test the use case described in this blog.

If you are using Authorization Code grant flow with PKCE, follow this video to setup a Cognito user pool and get the access token, ID tokens :

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

  • 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-test1.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 “Authorization code grant” checkbox for “Allowed OAuth flows”. Select all the scopes for “Allowed custom scopes” and save changes. Also, select email, openid and profile scopes which are used by OpenID connect protocol. If certain clients should have only “read_product” scope, then select only that checkbox. Also, add a dummy callback URL like https://localhost/callback. Please note that callback URL will be the client app’s URL which will receive the Authorization code in a real world scenario.
  • Now, we have successfully setup a OIDC agent in Cognito. Below CURL command should return an access token

Step 2 : Create Cognito user to test the Authorization code grant flow

  • Go to “General Settings > Users and groups” and select “Create user”.
  • Enter some test user credentials. You can uncheck all the checkboxes since this is only for testing purpose.
  • Now, we have to build the Authorization code grant flow URL. Below is the format of that URL :
Base domain URL (from previous step) + /oauth2/authorize/oauth2/authorize?response_type=code&client_id=<client ID from App client configuration>&redirect_uri=https://localhost/callback&identity_provider=COGNITO&scope=openid+profile+product-api/read_product+product-api/delete_product+product-api/create_productBased on this, it will be :https://api-product-test1.auth.us-east-1.amazoncognito.com/oauth2/authorize?response_type=code&client_id=6b09anoqh56v85ruv3hk9c317c&redirect_uri=https://localhost/callback&identity_provider=COGNITO&scope=openid+profile+product-api/read_product+product-api/delete_product+product-api/create_product
  • It will redirect to Cognito login page. You can enter the testuser1 credentials. It will show change password screen during first time login:
  • If the authentication is successful, it will redirect back to the callback URL https://localhost/callback with the code parameter. As mentioned earlier, it will be application’s URL in real world scenario.
  • To get the id_token, access_token and refresh_token, invoke the Cognito token endpoint :
curl -X POST --user 'client_id:client_secret' -H 'Content-Type: application/x-www-form-urlencoded' "https://api-product-test1.auth.us-east-1.amazoncognito.com/oauth2/token?grant_type=authorization_code&client_id=<client_id>&scope=openid+profile&redirect_uri=https://localhost/callback&code=<authorization_code>"It will return the following response :{"id_token":"<JwT ID Token>","access_token":"<JwT access token>","refresh_token":"<JwT refresh token>"}
  • 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”
  • You can also check the id_token JwT which will show all the user attributes

Part 3 : Deploy a sample API Gateway application with 3 HTTP methods — GET, POST, DELETE and static response

  • Go to API Gateway service and create a new REST API
  • In the next screen, create 3 HTTP methods using the Actions menu
  • Select each HTTP method and change the integration type to “Mock”
  • Now, for each method, select the Integration Response to modify the static response.
  • Expand the 200 response status and select “Mapping Templates > application / json”.
  • In the Generate template text box, enter a static response like below :
  • Deploy the API using “Actions > Deploy API”
  • copy the “Invoke URL” and check the response for each HTTP method
curl -X GET  https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Get product details"
}
────────────────────────────────────────────────────────────────────
curl -X POST https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Create a new product"
}
────────────────────────────────────────────────────────────────────
curl -X DELETE https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Delete a product"
}

Step 4: 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 5: Testing

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

  • Start the authorization code grant flow and get id_token, access_token and refresh_token as mentioned in Step 2.
  • Try testing the 3 curl requests from Step 3 and it should return unauthorized response:
curl -X GET  https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{"message":"Unauthorized"}
────────────────────────────────────────────────────────────────────
curl -X POST https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{"message":"Unauthorized"}
────────────────────────────────────────────────────────────────────
curl -X DELETE https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{"message":"Unauthorized"}
  • This is expected because the API now expects a access_token with valid scopes
  • Now, try passing the access_token in Authorization header and it should return successful response:
curl -X GET  -H 'Authorization: <access_token>' https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Get product details"
}
────────────────────────────────────────────────────────────────────
curl -X POST -H 'Authorization: <access_token>' https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Create a new product"
}
────────────────────────────────────────────────────────────────────
curl -X DELETE -H 'Authorization: <access_token>' https://<unique ID>.execute-api.us-east-1.amazonaws.com/dev
{
"statusCode": 200,
"message": "Delete a product"
}

This concludes the setup. By creating multiple clients with different scopes, API access can be controlled per client application and all the users of that application. One disadvantage with Cognito is that there is no way to restrict the scopes on a per user basis. For ex: In some identity providers like ForgeRock, Okta, Auth0 etc. , you can restrict the scopes on a per user basis based on a user’s attribute / group membership. One option would be to start the Authorization code grant flow using minimum scope like read access and get additional scopes from Cognito as and when user tries to perform the operation.

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.

--

--