Front-End Web & Mobile

How to enforce user quota on AWS AppSync with Lambda Authorizer

API Quotas define the valid amount of calls available for a consumer during a specific amount of time. Enforcing quotas protects your API from unintentional abuse, minimizes data exfiltration and protects your resources from excessive usage. Beyond the mentioned security benefits, it can also unlock your capabilities to monetize the digital assets sitting behind the API.

AWS AppSync is a fully managed service that makes it easy to develop GraphQL APIs by handling the heavy lifting of securely connecting to data sources like Amazon DynamoDB, AWS Lambda, and more. Once deployed, it automatically scales your GraphQL API execution engine up and down to meet API request volumes. AWS AppSync manages API quotas based on tokens that effectively measure how different GraphQL queries use resources depending on their complexity. The number of available tokens is a soft limit and can be increased with a support case. This blog post showcases building blocks for applying an additional extra layer to manage quotas  based on the number of API calls a user makes using AWS Lambda Authorizer.  Thanks to its sub-millisecond latency, Amazon ElastiCache for Redis  is used to track the call count in real time. For the end-to-end test of the solution, Amazon Cognito User Pool serves as the OIDC identity provider.

Solution Overview

Figure 1. Solution Overview

Figure 1. Solution Overview

The solution consists of the following main components:

  • Authentication – A Cognito User Pool serves as the OIDC Identity Provider. It stores a set of users that are allowed to consume the AWS AppSync API. A custom attribute “custom:quota” is added to the user profile in order to store the quota value. This attribute can be passed via the CLI at user creation or later by editing it. Please note that this solution is not bounded exclusively to Cognito specifications. With few adjustments you can opt in for another identity provider as long as it follows the OIDC protocol.
  • Authorization – A Lambda function acts as the custom authorizer for the AWS AppSync API. It contains the necessary logic to validate the access token provided by the user and authorize the call based on the “custom:quota” attribute. The current number of calls are tracked via an ElastiCache cluster running Redis engine, in the form of hash data type. Every day at midnight hash-es are expired from the cache. Both the Lambda function and the ElastiCache cluster are deployed inside an Amazon VPC on two private subnets. The pre-requisite section provides the necessary information on how to setup.
  • Interface – The API is hosted on AWS AppSync and runs the GraphQL engine. Data is sourced from a DynamoDB Table which hosts the moviedata.json dataset.  The GraphQL schema is defined according to this dataset attributes. A DynamoDB resolver is used to convert the GraphQL payload to DynamoDB specifications. Two operations are provided from the API. A query for consuming the dataset and a mutation to write new data.

Lambda Authorizer

Next, we explore the anatomy of the Lambda Function which acts as the authorizer for the AWS AppSync API.

The authentication logic and Cognitojwt library are encapsulated in a Lambda layer. From there we import the “auth” method which returns a dictionary (upon successful authentication) with the user attributes fetched from Cognito Userinfo endpoint. For our purpose we need to extract the “username” and “custom:quota” keys.

Figure 2. Authentication Method

Figure 2. Authentication Method

In the meantime, we open the connection to the redis database which we will use to keep track of the count of calls per user. Daily at 00:00 the entry for every user gets erased.

Figure 3. Redis Connection

Figure 3. Redis Connection

As a next step the function will query redis to check if the user has already an entry in the database. If the user exists, it will compare the current count to the quota value extracted earlier. If the count is below the quota value, it increments the count for the user by 1, updates the hash in the database and returns a positive response to authorize the query. On the other hand, if the current count is equal or above the quota value, it returns an un-authorized response. For users who are querying the API for the first time that day, the function creates a new hash into the database and sets it to expire by 00:00.

Figure 4. Response model/Quota check logic

Figure 4. Response model/Quota check logic

Walkthrough

Now we walk you through the various steps in implementing the solution.

Breakdown of the steps to successfully implement and test the solution:

  • Step 1: AWS CloudFormation stack deployment
  • Step 2: Upload the test data into DynamoDB
  • Step 3: Create a test user in Cognito user pool and add the custom attribute
  • Step 4: Run a test query

Prerequisites

For this walkthrough, you should have the following prerequisites:

Step 1: CloudFormation Stack

The resources are created via CloudFormation. The CloudFormation template and the source code for the Lambda function and layer are available in Github.

Following resources are part of the CloudFormation stack:

  • AWS AppSync GraphQL API
  • Amazon DynamoDB table
  • Cognito user pool
  • Lambda Authorization function
  • Lambda layer containing authentication logic
  • ElastiCache for Redis cluster
  • Security groups
  • AWS IAM roles & policies

The deployment is done using the deploy.sh script.

The script executes the following phases:

  1. Checks for dependencies
  2. Builds the Lambda layer
  3. Builds the Lambda package
  4. Builds the CloudFormation package
  5. Deploys the CloudFormation package

Following parameters are required for the deployment to be successful:

  • STACK_NAME – CloudFormation stack name
  • AWS_REGION – AWS region where the solution will be deployed
  • AWS_PROFILE – Named profile that will apply to the AWS CLI command
  • ARTEFACT_S3_BUCKET – S3 bucket where the infrastructure code will be stored. (The bucket must be created in the same region where the solution lives)
  • VPC – VPC id where the Lambda function and redis cluster will be deployed
  • SUBNET_A – Private subnet id where the Lambda function and redis node will be deployed
  • SUBNET_B – Private subnet id where the Lambda function and redis node will be deployed
The following commands can be used to run the “deploy.sh” script
chmod +x deploy.sh 
./deployment_scripts/deploy.sh STACK_NAME \
                       ARTEFACT_S3_BUCKET \
                   AWS_PROFILE AWS_REGION \
SUBNET_A SUBNET_B VPC \

Upon successful deployment, the script will display the CloudFormation output on the console. Take note of the output as it will be used in the following steps.

Step 2: Upload the test data into DynamoDB

In this step we are going to populate the DynamoDB table created with sample data to test the connection to the AWS AWS AppSync API.

To perform this step, we are going to use the script batch_upload_dbb.py

Following parameters are required for the script to run successfully:

  • DynamoDBTableName – Value Outputted by the CloudFormation deployment
  • AWS_PROFILE – Named profile that will apply to the AWS CLI command
  • AWS_REGION – AWS region where the solution will be deployed
  • moviedata.json – File already provided with sample data. Located in the git repository, here.
The following command can be used to run the “batch_upload_dbb.py”
python3 batch_upload_ddb.py -t DynamoDBTableName \
                                 -p AWS_PROFILE \
                                  -r AWS_REGION \
                              -f moviedata.json

Step 3: Create a Test User in Cognito user pool and add the custom attribute

In this step we are going to create a Cognito test user and add a custom attribute which will be read by the Lambda Function as an extra validation step during authorization.

Following parameters are required for the command to run successfully:

  • AWS_REGION – AWS region where the solution will be deployed
  • CognitoUserPoolId – Output value from the CloudFormation deployment

The following AWS CLI command can be used to create a new user in Cognito User Pool:

aws cognito-idp admin-create-user --user-pool-id CognitoUserPoolId \
    --region AWS_REGION \
    --username TestUser \
    --temporary-password <insert_temp_password> \
    --user-attributes Name=email,Value=testuser@example.com Name=custom:quota,Value=10 Name=email_verified,Value=true

aws cognito-idp admin-set-user-password --user-pool-id CognitoUserPoolId \
    --username TestUser \
    --password <insert_password> \
    --permanent \
    --region AWS_REGION

Upon successful run of the CLI Commands, a new user will be present in the user pool with a custom attribute quota value = 10.

Step 4: Run Test Query

In this step we are going to configure Postman and send a test query toward the AWS AppSync API, so you need to make sure Postman is installed in your machine. Upon successful authorization, the API will return the requested values. Once the quota attribute value is hit, all following requests will be DENIED automatically.

Following parameters are required to create a complete request in Postman

  • GraphQLUrl – Output Value from CloudFormation deployment
  • CognitoAuthUrl – Output Value from CloudFormation deployment
  • CognitoAccessTokenUrl – Output Value from CloudFormation deployment
  • CognitoAppClientId – Output Value from CloudFormation deployment
  • CognitoAppClientSecret – This value needs to be retrieved from the AWS console, in the Cognito user pool service as described here

Follow these steps to setup Postman

  1. Start Postman in your machine
  2. Go to File -> Import
  3. Import the postman collection
  4. From the collection folder navigate to Sample Request
  5. Replace all variables in the Sample Request with the parameters mentioned above.
  6. Navigate to the Authorization tab
  7. Click on Get New Access Token
    1. Postman will pop up a new window where you need to type the username and password for the user previously created via CLI
    2. Once authenticated, a new token will appear in Postman.
    3. Click on Use Token to add it to the request
  1.  Click Send on the request to POST the HTTP/s request to the API
    1. The API will return in the body of the response a JSON datatype similar to:
{
        "data": {
            "getMovie": {
                "info": {
                    "actors": [
                        "Fay Wray",
                        "Robert Armstrong",
                        "Bruce Cabot"
                    ],
                    "directors": [
                        "Merian C. Cooper",
                        "Ernest B. Schoedsack"
                    ],
                    "genres": [
                        "Adventure",
                        "Fantasy",
                        "Horror"
                    ]
                }
            }
        }
    }
  1. Click on Send multiple times to surpass the quota attribute value added to the user.
    1. Once the quota limit is hit, the following response will be sent by the API:
{
    "errors": [
        {
            "errorType": "UnauthorizedException",
            "message": "You are not authorized to make this call."
        }
    ]
}

Cleaning up

To avoid incurring future charges, delete the resources. You can delete the CloudFormation stack by running destroy.sh script.

Following parameters are required to run the script successfully

  • STACK_NAME – CloudFormation stack name
  • AWS_REGION – AWS region where the solution will be deployed
  • AWS_PROFILE – Named profile that will apply to the AWS CLI command

The following commands can be used to run the “destroy.sh” script:

./deployment_scripts/destroy.sh STACK_NAME AWS_PROFILE AWS_REGION

Conclusion

In this blog post, we showed how to apply quota enforcement on AWS AppSync using a Lambda Authorizer with the aid of ElastiCache for Redis. We dove deep in the Lambda function and the authorization logic. Step by step, we walked through the implementation phases. Finally, we showed how to test the solution end-to-end.

If you want to learn more about AWS AppSync, please follow the official documentation.

“Get started with AWS AppSync for free.” https://thinkwithwp.com/appsync/

About the authors:

Edvin Hallvaxhiu

Edvin Hallvaxhiu is a Sr. Global Security Architect with AWS Professional Services and is passionate about cybersecurity and automation. He helps customers build secure and compliant solutions in the cloud. Outside work, he likes traveling and sports.

Bruno Dhefto

Bruno Dhefto is a Global Security Architect with AWS Professional Services. He is focused on helping customers building Secure and Reliable architectures in AWS. Outside of work, he is interested in the latest technology updates and traveling.

Gezim Musliaj

Gezim Musliaj is a DevOps Consultant with AWS Professional Services. He is interested in all things data and their application in the field of IoT, Massive Data Ingestion and Analytics. Outside work, he is a big automotive fan and likes long trips with car.