Protect API Gateway with Amazon Verified Permissions
![This post cover image](/assets/img/post-secure-api-gateway-avp/cover-image.png)
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
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.
From here select the Groups tab and create the two groups, trainers and riders.
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.
If you click the unicorn service api you should see the created resources.
Selecting one of the resources will show that no auth is configured.
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.
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.
Now we need to select the API and Stage and import the API.
Next step is now to select the Identity Source and in this case it will be the Cognito User Pool.
Final step is to assign what actions a specific group should have access to in the API.
Now, we are all set and ready to create the 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.
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.
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!
Test what you just learned by doing this five question quiz - https://kvist.ai/752211.
Scan the QR code below or click the link above.
Powered by kvist.ai your AI generated quiz solution!