AWS AppSync Authorization using Lambda

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 AWS DynamoDB, Lambda, and more.

With AppSync, you create GraphQL APIs that your applications interact with over the internet. While the API endpoints are publicly reachable, they never allow unauthorized access.

I will create and deploy a lambda function in Node.js with AWS CDK. The AWS CDK is a tool that makes it easy to create your infrastructure through the code.

Prerequisites

  • An AWS account
  • AWS CLI configured (check out this link to see how to do it)
  • NodeJS installed on your computer

Lambda function to authorize GraphQL API calls

Let's create the function to be executed when authorizing GraphQL API calls in AppSync:

mkdir appsync-lambda-authorizer
cd appsync-lambda-authorizer
cdk init --language typescript
Initialize the project with the CDK

The command will generate many files and install the necessary dependencies. At the root level of the CDK project , you should find the lib folder with the file called appsync-lambda-authorizer-stack.ts. Let's update the file as follows:

import { Stack, StackProps, Duration } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as path from 'path';

export class AppsyncLambdaAuthorizerStack extends Stack {
    constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);

        const vpc = new ec2.Vpc(this, "thabolebeloVPC", {
            maxAzs: 2,
            natGateways: 1,
            subnetConfiguration: [
                {
                    name: 'private-subnet-1',
                    subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
                    cidrMask: 24,
                },
                {
                    name: 'public-subnet-1',
                    subnetType: ec2.SubnetType.PUBLIC,
                    cidrMask: 24,
                },
            ],
        });

        const lambdaFunction = new lambda.Function(this, 'lambda-function', {
            runtime: lambda.Runtime.NODEJS_14_X,
            vpc,
            vpcSubnets: {
                subnetType: ec2.SubnetType.PRIVATE_WITH_NAT,
            },
            memorySize: 1024,
            timeout: Duration.seconds(300),
            handler: 'index.handler',
            functionName: 'AppSyncAuthorizer',
            code: lambda.Code.fromAsset(path.join(__dirname, '/../src/')),
        });
    }
}
Lamda function stack

The handler property indicates the entry file then the function to run inside this file. So the index.handler can be broken down to:

  • index: the file called index.js inside the src directory.
  • handler: the function inside the index.js to execute.

Lets go over what the code will provision:

  1. A VPC with a Public and a Private subnet group,  this VPC will also provision 1 NAT Gateway which will allow our lambda to access the internet from a Private subnet
  2. A lambda function called AppSyncAuthorizer that will be placed in a Private subnet in the VPC. The code for the lambda function is located in the src folder at the project root.

Let's add the code for the lambda function, name the file index.js

exports.handler = async (event) => {
    console.log(`event >`, JSON.stringify(event, null, 2))
    const {
        authorizationToken,
        requestContext: { apiId, accountId, requestId },
    } = event
    const response = {
        isAuthorized: authorizationToken === 'thabolebelo',
        ttlOverride: 10,
    }
    console.log(`API_ID = `, apiId)
    console.log(`ACCOUNT_ID = `, accountId)
    console.log(`REQUEST_ID = `, requestId)
    console.log(`AUTH_TOKEN = `, authorizationToken)
    return response
}
Lambda function authorization logic 

AppSync receives the Lambda authorization response and allows or denies access based on the isAuthorized field value:

  • The function checks the authorization token and, if the value is thabolebelo , the request is allowed.
  • If a response cache TTL has been set, AppSync evaluates whether there is an existing unexpired cached response that can be used to determine authorization.

Deploy the Lambda function

The first step to deploy the Lambda is to generate the CloudFormation template from the CDK code. This command will output the CloudFormation stack to be created in the console:

cdk synth

Once done, we can deploy our app:

cdk deploy

After a deployment, we can see that the lambda has been launched in a VPC, and is associated to private subnets only.

Everything seems to be in place for our Lambda function launched in a VPC to have internet access.

Test the Lambda function

AppSync sends the request authorization event to the Lambda function for evaluation in the following format:

{
    "authorizationToken": "ExampleAUTHtoken123123123",
    "requestContext": {
        "apiId": "aaaaaa123123123example123",
        "accountId": "111122223333",
        "requestId": "f4081827-1111-4444-5555-5cf4695f339f",
        "queryString": "mutation CreateEvent {...}\n\nquery MyQuery {...}\n",
        "operationName": "MyQuery",
        "variables": {}
    }
}
This will be out test event

Head over to the deployed lambda function on AWS console in order to test the authorization:

Open the lambda function then click on the test button to configure the request's payload. A modal will popup:

The token our test event sends is ExampleAUTHtoken123123123 which will not approve the API call. We only get access when token is thabolebelo. Let's test the function with the test event we just configured:

We have successfully deployed a Lambda function with the CDK. Run cdk destroy to destroy all the resources created in AWS. The code for this post is available on this repo.

I will build on this solution in a future post. We will create an Angular application that consumes a GraphQL API, we will authorize the client application using this solution!