Protect API Gateway with Amazon Verified Permissions
Tags: aws, serverless, security
Published: 2024-04-12 05:52:33

---
title: Protect API Gateway with Amazon Verified Permissions
description: In this post we'll look at Amazon Verified Permissions (AVP) a serverless service for easy management and enforcement of application permissions, and how to use it to secure Amazon API gateway API together with Cognito User Pools.
cover_image: https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/cover-image-dev.png
tags: aws, serverless, security
canonical_url: https://jimmydqv.com/secure-api-gateway-avp/index.html
published: true
---

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](https://aws.amazon.com/about-aws/whats-new/2024/04/amazon-cognito-customers-access-apis-verified-permissions/) 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](https://jimmydqv.com/secure-api-gw-with-auth0/), [Secure your API Gateway APIs mutual TLS](https://jimmydqv.com/secure-api-gw-with-mutual-tls/), and [Secure your API Gateway APIs with Lambda Authorizer](https://jimmydqv.com/secure-api-gw-with-lambda-auth/).

## Architecture overview

![Architecture Overview](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/architecture-overview.png)

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.

``` yaml

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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/user-pool-list.png)

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

![Console showing user pool create groups](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/user-pool-create-groups.png)

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.

``` yaml
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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/api-list.png)

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

![Console showing API resources](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/api-resources.png)

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

![Console showing API has no auth](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/api-no-auth.png)

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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-create.png)

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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-specify-policy-store.png)

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

![Console showing API and Stag](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-import-api-resources-1.png)

![Console showing API paths](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-import-api-resources-2.png)

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

![Console showing IDP source](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-choose-idp.png)

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

![Console showing action selection](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-select-actions-1.png)

![Console showing action selection](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-select-actions-2.png)

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

![Console showing create policy store](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-create-policy-store.png)

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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/avp-create-policy-store-error.png)

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](https://jimmydqv.com/assets/img/post-secure-api-gateway-avp/api-no-auth.png)

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](https://www.linkedin.com/in/dahlqvistjimmy/) and [X](https://x.com/jimmydahlqvist) for more content, and read rest of my [Blogs](https://jimmydqv.com)

As Werner says! Now Go Build!