DEV Community

Cover image for Protect API Gateway with Amazon Verified Permissions
Jimmy Dahlqvist for AWS Community Builders

Posted on • Originally published at jimmydqv.com

Protect API Gateway with Amazon Verified Permissions

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
swolebrain profile image
Victor Moreno

Hey, thanks for the awesome post! We already fixed the bug with the dashes; we automatically strip them.

Collapse
 
jimmydqv profile image
Jimmy Dahlqvist

Awesome!