SAM with custom authoriser locally (episode 1)

SAM with custom authoriser locally (episode 1)

Background

When you want to develop an AWS Serverless application, you may have heard of this powerful tool - SAM CLI (Serverless Application Model). You can use SAM to create a classic serverless application (including API Gateway and Lambda) on AWS. Also, it can simulate the AWS environment on your local environment.

Recently, SAM CLI added a useful functionality to support simulating API Gateway Custom Authoriser locally. Before this feature was implemented, it can be painful to develop an API Gateway with Custom Authoriser. This post will walk you through an example, and help you use this new feature with ease!

TL;DR

Let's walk through it

We are going to create an API called Ping with TypeScript. When you invoke this API with path /ping, your request goes through a custom authoriser called Authorizer. If the custom authoriser returns a valid response, your request can reach the Lambda of Ping API.

The architecture diagram as following:

sam-api-gateway-custom-authoriser-example.drawio

We have four steps to implement this example:

  1. Creating a Basic API Gateway
  2. Creating a Ping Lambda
  3. Creating an Authoriser Lambda
  4. Verifying the Result

Creating a Basic API Gateway

You can create a brand new project with sam init and remove the stuff you don't need. Or, if you want a minimal working project with TypeScript, you can clone sam-local-authorizer-example.

After you got a SAM project, the first thing you need to do is defining an API Gateway in {project_root_path}/template.yaml.

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  sam-local-authorizer-example
Transform:
- AWS::Serverless-2016-10-31

Resources:
  # API Gateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Auth:
        # CORS setting
        AddDefaultAuthorizerToCorsPreflight: false
        ResourcePolicy:
          CustomStatements: [
              {
                "Effect": "Allow",
                "Principal": "*",
                "Action": "execute-api:Invoke",
                "Resource": "execute-api:/*/OPTIONS/*",
              },
            ]

Creating a Ping Lambda

We are going to create a Ping Lambda which response pong when it's invoked. Also, we are going to explore how to develop Lambda with TypeScript.

Let's add the Ping Lambda first!

Create a new file {project_root_path}/src/handlers/ping.ts with the following codes.

import { APIGatewayProxyHandler } from 'aws-lambda';

export const lambdaHandler: APIGatewayProxyHandler = async (event, context) => {
    return {
        statusCode: 200,
        body: "pong",
    }
};

In order to expose the Ping Lambda we created, we need to create another file {project_root_path}/src/app.ts with the following codes.

import { lambdaHandler as PingHandler } from './handlers/ping';


export {
    PingHandler,
}

Now, we got a function ready to handle the Lambda invocation event. But, this is a TypeScript Lambda, we need a file {project_root_path}/package.json as well. The following code is an example of package.json

{
  "name": "sam_local_authorizer_example",
  "version": "1.0.0",
  "description": "lambda",
  "main": "app.js",
  "repository": "",
  "author": "",
  "license": "MIT",
  "dependencies": {},
  "scripts": {
    "build": "sam build",
    "deploy": "sam deploy"
  },
  "devDependencies": {
    "@tsconfig/node18": "^1.0.0",
    "@types/aws-lambda": "^8.10.73",
    "@types/node": "^18.0.0",
    "typescript": "^4.2.3"
  }
}

Finally, let's define this Ping Lambda by adding a new AWS::Serverless::Function resource under Resources section in {project_root_path}/template.yaml.

... Others
Resources:
  ... API Gateway
  # Protected API Lambda
  Ping:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2020
        SourceMap: false
        External:
          - node_modules
        EntryPoints:
          - src/app.ts
    Properties:
      Runtime: nodejs18.x
      CodeUri: ./
      Handler: app.PingHandler
      Events:
        Api:
          Type: Api
          Properties:
            Auth:
              ApiKeyRequired: true
            RestApiId:
              Ref: ApiGateway
            Path: /ping
            Method: get

As you can see, the Ping resource has a section Metadata that tells SAM to use esbuild for your Lambda. Before you run sam build, you may need to make sure you install the esbuild beforehand.

You can use the following command to install esbuild and try to build the Ping Lambda.

npm install -g esbuild
sam build

Now, you should be able to see a new folder {project_root_path}/.aws-sam/build/Ping/app.js contains the transpiled JavaScript.

Creating an Authoriser Lambda

We are going to create a simple Authoriser Lambda in  {project_root_path}/src/handlers/authorizer.ts. This Lambda allows all requests to access GET/ping API.

import { APIGatewayTokenAuthorizerHandler, APIGatewayAuthorizerResult } from 'aws-lambda';

export const lambdaHandler: APIGatewayTokenAuthorizerHandler = async (event, context) => {
  return generateAdminPolicy();
};

const generateAdminPolicy = () => {
  const authResponse: APIGatewayAuthorizerResult = {
    principalId: `systemadmin`,
    policyDocument: {
      Version: '2012-10-17',
      Statement: [
        {
          Action: 'execute-api:Invoke',
          Effect: 'Allow',
          Resource: 'arn:aws:execute-api:*:*:*/*/GET/ping',
        },
      ],
    },
  };
  return authResponse;
}

After we created the Authoriser Lambda, we need to expose this Lambda in {project_root_path}/src/app.ts as well.

import { lambdaHandler as AuthorizerHandler } from './handlers/authorizer';
import { lambdaHandler as PingHandler } from './handlers/ping';


export {
    AuthorizerHandler,
    PingHandler,
}

We are almost there! 🚩

Let's define the Authoriser Lambda in {project_root_path}/template.yaml. In order to make the SAM work with Authoriser Lambda locally, the definition of template.yaml is crucial.

We need to add two attributes DefaultAuthorizer and Authorizers to AWS::Serverless::Api resource.

Then, we need to define Authorizer Lambda under Resources section.

... Others
Resources:
  # API Gateway
  ApiGateway:
    Type: AWS::Serverless::Api
    Properties:
      StageName: dev
      Auth:
        ... Others
        DefaultAuthorizer: LambdaTokenAuthorizer
        Authorizers:
          LambdaTokenAuthorizer:
            FunctionPayloadType: TOKEN
            FunctionArn: !GetAtt Authorizer.Arn
            Identity:
              Header: Authorization
              ReauthorizeEvery: 300
  # Auth Lambda
  Authorizer:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2020
        SourceMap: false
        External:
          - node_modules
        EntryPoints:
          - src/app.ts
    Properties:
      Runtime: nodejs18.x
      CodeUri: ./
      Handler: app.AuthorizerHandler
      Timeout: 5
   ... Ping Lambda

Done!🎉 You are all set. Let's jump to the next step to verify the result.

Verifying the Result

Now, you can run the following command to simulate API Gateway and Lambda Authoriser locally.

sam build
sam local start-api

Screenshot-2023-08-11-220411

Your API Gateway is listening on port 3000. Using the following command to send a request and see it in action.

curl -H "Authorization: abc" http://127.0.0.1:3000/ping

Screenshot-2023-08-11-220803

Cheers! 🍺 That's all. Enjoy the API Gateway and Custom Authoriser locally.

What Is Next?

Check the AWS Serverless - Using Serverless Framework with custom authoriser locally (episode 2) to find out another amazing tool.