Type-Safe AWS Lambda Authorizer for ApiGateway V2 with TypeScript
In this article, we'll explore a neat solution to define your types and ensure perfect type validation between your authorizer and service lambdas for apiGateway V2
Specification
The following is an implementation using:
- API Gateway v2 (HTTP API)
- Authorizer Lambda with payload v2
- Full AWS TypeScript
Why?
Why Do I Need Perfect TypeScript Integration Between Authorizer Lambda and Service Lambda?
Good TypeScript checks will prevent errors more effectively than any unit tests in this scenario. If you ever change one of your components—either the authorizer or service lambda—TypeScript will help you identify all affected services before you deploy and cause downtime. Unit testing in this situation will just hardcode your lambda with what you have at that time and not evolve to reflect the changes of different components during the development workflow over a longer period.
Why Should I Use Response Payload v2 vs V1?
It is cleaner, simpler, and suits my project needs better. More technical context on it can be found on aws official docs
Why Should I Use HTTP API Instead of API Gateway v1?
It is faster and cheaper. However, read its limitations before picking it; it’s more of a proxy endpoint than a full-fledged API with additional features for checks. Docs with comparison
Full GitHub with an example can be found [here](https://github.com/bogdan-largeanu/blog-demo-typescript-apigatewayv2-authorizer).
Step-by-Step Guide
Step 1: Create a Lambda
1. Define the Interface
We first create a lambda authorizer. Let's define the interface. I will explain `contextAuth2` in a second. A simple authorizer must adhere to the following format AWS Documentation
interface IResponseAuthSimplePayloadv2 {
isAuthorized: boolean
context: contextAuth2
}
interface for auth
2. Define the Context Object
`contextAuth2` is the object we want to pass from our authorization lambda to our service lambda. It can be anything; a common pattern is to pass the `userId`. We will save this in a separate file so we can reference it from both the authorizer and service lambda.
export interface contextAuth2 {
userId: string;
};
the interface we pass between auth lambda and service lambda
3. Type Definition for Authorizer
For our authorizer, we define both the input and output response to ensure our type is recognized as an authorizer lambda with a simple v2 payload and only accepts our custom object defined in `contextAuth2`.
const authorizerHandler = async (
authorizerEventV2: APIGatewayRequestAuthorizerEventV2,
): Promise<APIGatewaySimpleAuthorizerWithContextResult<contextAuth2>> => {}
4. Combine Everything for the Lambda Authorizer
Let's put everything together for our lambda authorizer.
import {
APIGatewayRequestAuthorizerEventV2,
APIGatewaySimpleAuthorizerWithContextResult,
} from 'aws-lambda';
import {contextAuth2 } from './IAuthorizerContext'
// format simple authorizer must be https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-lambda-authorizer.html
interface IResponseAuthSimplePayloadv2 {
isAuthorized: boolean
context: contextAuth2
}
const authorizerHandler = async (
authorizerEventV2: APIGatewayRequestAuthorizerEventV2,
): Promise<APIGatewaySimpleAuthorizerWithContextResult<contextAuth2>> => {
// We set the template of the response. IsAuthorized is mandatory
// while context is option and up to us to implement there whatever values we want to pass to our service lambda
const response : IResponseAuthSimplePayloadv2= {
isAuthorized: false,
context: {
userId: '',
// if add another field here we will get an error
},
};
const authToken = authorizerEventV2.identitySource[0];
// will assume the token is valid for the purpose of the demo, but here's where you implement your validation on the authToken
if(authToken){
response.isAuthorized = true
response.context.userId = "userIdExtractedFromAuthToken"
// if add another field here on context we will also get an error too since is not defined in the context interface
// example : response.context.unexpectedField = 1
}
return response;
};
module.exports = { authorizerHandler };
Step 2: Reuse the Interface in Our Service Lambda
1. Combine Expected Event Types and Custom Context
We build a type that combines both the expected event types and our custom context passed inside the lambda.
type CustomAuth = APIGatewayAuthorizerResultContext & contextAuth2
2. Pass the Custom Type into Handler Interface
We pass this into our handler interface to get all the lambda event types + authorizer v2 with simple response + our custom context.
const handler: APIGatewayProxyHandlerV2WithLambdaAuthorizer<CustomAuth> = async (
event
) => { }
3. Assemble Everything in the Service Lambda
import {
APIGatewayProxyHandlerV2WithLambdaAuthorizer,
APIGatewayAuthorizerResultContext,
} from 'aws-lambda';
import { contextAuth2 } from "./IAuthorizerContext"
interface requestBody {
file_no: string;
}
// We build a type that combines both the expected Event types and our custom context passed inside the lambda
type CustomAuth = APIGatewayAuthorizerResultContext & contextAuth2
const handler: APIGatewayProxyHandlerV2WithLambdaAuthorizer<CustomAuth> = async (
event
) => {
const extractUserIdFromAuth = () => {
// if lambda is run locally authorization lambda won't run the provide the trintId
if (process.env.IS_OFFLINE) return 'offline-lambda-trint-id';
// This is not automatically typescript checked. If I tried to access a property on lambda. that dose not exist we will get a type error as we should
// example : `event.requestContext.authorizer.lambda.banana`. This also mean if we change our interface in authorizer, any lambda importing this interface will throw a type error to warn us we need to refactor the code
const userId = event.requestContext.authorizer.lambda.userId;
return userId;
};
const unmarshelledBody: requestBody = JSON.parse(event.body || '');
const userId = extractUserIdFromAuth();
const {
file_no: fileNo,
} = unmarshelledBody;
return {
isBase64Encoded: false,
statusCode: 200,
headers: { 'lambda-version': `1.1` },
body: JSON.stringify({
userIdFromAuthorizerLambda: userId,
fileNo
}),
};
};
module.exports = { handler };
Now any future changes to our authorizer, such as adding a new field or removing an existing one, will trigger TypeScript errors, helping us refactor the code accordingly.
This was done using serverless, but the same can be deployed with SAM or Terraform.
service: demo-typescript
package:
individually: true
excludeDevDependencies: true
plugins:
- serverless-esbuild # must be before offline
# - serverless-prune-plugin
- serverless-offline
- serverless-iam-roles-per-function
custom:
#Bundler
esbuild:
minify: false
packager: yarn
watch:
pattern: ['.src/index.ts', 'src/**/*.ts']
ignore: ['.serverless/**/*', '.build']
config: './esbuild.config.js'
provider:
name: aws
runtime: nodejs14.x
stage: ${opt:stage}
region: ${opt:region}
# Tags
stackTags: &stackTags
ServiceGroup: DemoTypescriptAuthv2
AppId: DemoTypescriptAuthv2
tags: *stackTags
# Logs and metrics
logRetentionInDays: 365
logs:
httpApi: true #enable logging for apigateway v2
httpApi:
metrics: true #enables detailed metric
useProviderTags: true
# Authorizer
authorizers:
CustomLambdaAuthorizer:
type: request # Should be set to 'request' for custom Lambda authorizers
functionName: AuthorizerDemoTypescript
resultTtlInSeconds: 300 # max is 1h = 3600.
enableSimpleResponses: true # # Set if authorizer function will return authorization responses in simple format (default: false)
payloadVersion: '2.0' #
identitySource:
- $request.header.Authorization
# Optional. Applicable only when using externally defined authorizer functions
# to prevent creation of permission resource. Set this to true if you use this authorizer in another serverless service
managedExternally: false
functions:
AuthorizerDemoTypescript:
memorySize: 256
timeout: 10
handler: src/authorizer.authorizerHandler
PresignedUrls:
handler: src/index.handler
memorySize: 256
timeout: 10
events:
- httpApi:
method: POST
path: /demo-typescript-authorizer
authorizer: # Optional
# Name of an authorizer defined in 'provider.httpApi.authorizers'
name: CustomLambdaAuthorizer
Full GitHub with an example can be found here.
https://github.com/bogdan-largeanu/blog-demo-typescript-apigatewayv2-authroizer