Protect API Gateway with Amazon Verified Permissions

2024-04-10
This post cover image
Voice provided by Amazon Polly

Amazon Verified Permissions (AVP) was presented during re:Inforce 2022. AVP is a fully managed serverless service that simplifies managing and enforcing application permissions. It uses Cedar policy language, which is one fastest growing policy language at the moment.

AVP is the perfect service to use for implementation of you application permissions. It's a great tool when implementing a centralized policy decision point (PDP). Isolation of tenants in a SaaS solution is made easy with AVP.

Up until now, protecting your API Gateway API with Amazon Cognito User Pools and AVP has been hard to do. With this new feature release this is now made easy.

In this post we'll look at this new feature and continue my series on securing API Gateway, Secure your API Gateway APIs with Auth0, Secure your API Gateway APIs mutual TLS, and Secure your API Gateway APIs with Lambda Authorizer.

Architecture overview

Architecture Overview

There are three parts in this setup. Amazon Cognito User Pool, API Gateway and Amazon Verified permissions decision endpoint. Users will call Cognito to logon and get their tokens. The tokens will be sent to API Gateway API in the Auth header, and we'll have a Lambda Authorizer to call AVP to verify the access.

The first thing we need to do is create an Cognito User Pool.

Cognito User Pool

Before we can continue the first thing we need is an Cognito User Pool. Let's create the pool using the below CloudFormation template.


AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Creates the User Pool
Parameters:
UserPoolName:
Type: String
Description: The name of the user pool
Default: my-unicorn-service
HostedAuthDomainPrefix:
Type: String
Description: The domain prefix to use for the UserPool hosted UI <HostedAuthDomainPrefix>.auth.[region].amazoncognito.com
Default: unicorn-service
CallbackDomain:
Type: String
Description: The domain used for signin callback
Default: localhost:8080

Resources:
##########################################################################
# UserPool
##########################################################################
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UsernameConfiguration:
CaseSensitive: false
AutoVerifiedAttributes:
- email
UserPoolName: !Ref UserPoolName
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
- Name: name
AttributeDataType: String
Mutable: true
Required: true

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
GenerateSecret: True
AllowedOAuthFlowsUserPoolClient: true
CallbackURLs:
- !Sub https://${CallbackDomain}/signin
AllowedOAuthFlows:
- code
- implicit
AllowedOAuthScopes:
- phone
- email
- openid
- profile
SupportedIdentityProviders:
- COGNITO

HostedUserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: !Ref HostedAuthDomainPrefix
UserPoolId: !Ref UserPool

Outputs:
CognitoUserPoolID:
Value: !Ref UserPool
Description: The UserPool ID
CognitoAppClientID:
Value: !Ref UserPoolClient
Description: The app client
CognitoUrl:
Description: The url
Value: !GetAtt UserPool.ProviderURL
CognitoHostedUI:
Value: !Sub https://${HostedAuthDomainPrefix}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${UserPoolClient}&response_type=code&scope=email+openid+phone+profile&redirect_uri=https://${CallbackDomain}/signin
Description: The hosted UI URL

When the user pool is created we need to create two groups, trainers and riders. Let's do this from the console this time, navigate to Cognito section, locate the user pool and click on it.

Console showing list of user pools

From here select the Groups tab and create the two groups, trainers and riders.

Console showing user pool create groups

Next up, create the API.

Api Gateway API

Next part we need is the Api Gateway API. Right now it's only REST api that is supported, hopefully there will be support for HTTP api in the future as well.

Let's use CloudFormation and SAM to create the API that we need.

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: Create the unicorn service api

Globals:
Function:
Timeout: 5
MemorySize: 128
Runtime: python3.9

Resources:
LambdaRiderGet:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaRiderGet!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /rider
Method: get
RestApiId:
Ref: ApiRegional

LambdaRiderPost:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaRiderPost!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /rider
Method: post
RestApiId:
Ref: ApiRegional

LambdaRiderList:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaRiderList!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /riders
Method: get
RestApiId:
Ref: ApiRegional

LambdaTrainerGet:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaTrainerGet!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /trainer
Method: get
RestApiId:
Ref: ApiRegional

LambdaTrainerPost:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaTrainerPost!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /trainer
Method: post
RestApiId:
Ref: ApiRegional

LambdaTrainerList:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaTrainerList!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /trainers
Method: get
RestApiId:
Ref: ApiRegional

LambdaUnicornGet:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaUnicornGet!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /unicorn
Method: get
RestApiId:
Ref: ApiRegional

LambdaUnicornPost:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaUnicornPost!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /unicorn
Method: post
RestApiId:
Ref: ApiRegional

LambdaUnicornList:
Type: AWS::Serverless::Function
Properties:
Handler: index.lambda_handler
InlineCode: |
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message" : "Hello from LambdaUnicornList!"}),
}

Events:
HelloWorld:
Type: Api
Properties:
Path: /unicorns
Method: get
RestApiId:
Ref: ApiRegional

ApiRegional:
Type: AWS::Serverless::Api
Properties:
Name: unicorn-service-api
StageName: prod
EndpointConfiguration: REGIONAL

This will create an REST Api Gateway API with nine resources.

Console showing list of API

If you click the unicorn service api you should see the created resources.

Console showing API resources

Selecting one of the resources will show that no auth is configured.

Console showing API has no auth

Now both the prerequisites are created and we can move over to the actual securing using Amazon Verified Permissions.

Amazon Verified Permissions

This part of the setup will purely manual as AVP has released the quick start, to help us configure Authorization for APIs using verified permissions.

The first thing we need to do is open Amazon Verified Permissions section of the console, from here we'll start creating a policy store.

Console showing verified permissions

In this first step in the guide we select that we like to use Cognito and API Gateway, both need to be in place already. If they are missing there will be a message informing that.

Console showing specify details

Now we need to select the API and Stage and import the API.

Console showing API and Stag

Console showing API paths

Next step is now to select the Identity Source and in this case it will be the Cognito User Pool.

Console showing IDP source

Final step is to assign what actions a specific group should have access to in the API.

Console showing action selection

Console showing action selection

Now, we are all set and ready to create the policy store.

Console showing create policy store

So far this has been a very nice flow and very easy to use. But! Now the problem started for me. The first thing was that I got an error with an invalid character in the namespace.

Console showing create policy store error

This happens since I used '-' in the name if the API Gateway API, I named it 'unicorn-service-api' and this is used as the name space and was not allowed. I think the guide should have warned me about this earlier. The first step in the guide checks that there is an User Pool and an API Gateway API, it could also check the name requirements.

After solving this problem, renaming the API Gateway API, the error was gone.

Connect auth

After the policy store has been created the Lambda Authorizer must be connected to the API Gateway actions. Navigate to the API Gateway API and select Resources in the menu.

Console showing API Gateway Auth

Select the action and then click on Edit for 'Method request settings'. In the drop down menu select the Lambda Authorizer. Repeat this for all of the actions in the API.

Final step is then to Deploy the API to your stage again, changes do not take affect until this is done.

With this connection, you are done and your API is protected with Cognito User Pool and Amazon Verified Permissions.

Improvements

I think this is a nice addition and it makes creation of the Lambda Authorizer easier, however I do see a couple of areas for improvements.

First. Today the guide will deploy a CloudFormation template with the Lambda Authorizer. To connect this authorizer to API Gateway API this must be done manually in the console. Since I'm a user of AWS SAM a Authorizer must be in the same template / stack as the API Gateway resource, so it's not possible to import from a different stack. Instead of this automatic deployment I would prefer to get an option to download the code and template in either yaml or json. That way I could include this in the same template as the API Gateway resource and then handle the connection automatically.

Second. After creation of the Policy Store and the policies there are no easy way to add additional API actions to the policy, or to add a new group in the User Pool. APIs change and I would prefer an as easy way to update as it was to create.

Last. Better checks on conditions like the problem above that gave me an error.

Final Words

This was a quick walkthrough of the new feature to easy connect Amazon Verified Permissions to an API Gateway API. The process is easy to use and straight forward, but I do see improvement potential, and hopefully the guide will evolve over time.

Don't forget to follow me on LinkedIn and X for more content, and read rest of my Blogs

As Werner says! Now Go Build!