Photo by Muhammad Zaqy Al Fattah on Unsplash

Serverless Authorisation 🚀

Part 1 — Examples of authorisation in serverless solutions alongside both machine to machine and user authentication, using Cognito, Lambda Authorizers, Middy & Lambda Layers; with examples and code repo written in the AWS CDK and TypeScript.


This article covers how to implement authorisation (AuthZ) when it comes to your Serverless applications, and how it can be used in conjunction with authentication (AuthN). They are distinctly different approaches which we discuss below.

“Authentication and authorisation might sound similar, but they are distinct security processes in the world of identity and access management (IAM).

Authentication confirms that users are who they say they are. Authorisation gives those users permission to access a resource. “ —

We are going to discuss our fictitious company ‘LJG Sushi’ which allows both Customers and External Companies (B2B) to place orders, and users to also validate vouchers, with a supporting code repository to walk through.

This is Part 1 of the series, with subsequent parts focusing on AppSync Lambda Authorizers and using Lambda@Edge for authorisation.

The code repo for this article can be found here:

👇 Before we go any further — please connect with me on LinkedIn for future blog posts and Serverless news:

⚠️ Note: The CDK and code examples are to explain the concepts only and are not production ready. I have also added verbose comments and console logs to the code for ease of understanding the concepts and playing with the deployed code (including looking at logs).

What options do we have for Authorisation? 🙋

When it comes to authorisation in Serverless we have different options depending on our requirements, which include:

  1. Cognito User Pool Authentication + Lambda Authorisation.
  2. Lambda Authorizer Authentication & Authorisation.
  3. Lambda Authorizer Authentication + Lambda Authorisation.

Before we go any further, what is a Lambda Authorizer? 💭

Photo by bruce mars on Unsplash

A Lambda authorizer (formerly known as a custom authorizer) is an API Gateway feature that uses a Lambda function to control access to your API.

A Lambda authorizer (formerly known as a custom authorizer) is an API Gateway feature that uses a Lambda function to control access to your API.

A Lambda authorizer is useful if you want to implement a custom authorisation scheme that uses a bearer token authentication strategy such as OAuth or SAML, or that uses request parameters to determine the caller’s identity.

When a client makes a request to one of your API’s methods, API Gateway calls your Lambda authorizer, which takes the caller’s identity as input and returns an IAM policy as output. This is shown below:

There are two types of Lambda authorizers:

  • A token-based Lambda authorizer (also called a TOKEN authorizer) receives the caller's identity in a bearer token, such as a JSON Web Token (JWT) or an OAuth token.
  • A request parameter-based Lambda authorizer (also called a REQUEST authorizer) receives the caller's identity in a combination of headers, query string parameters, stageVariables, and $context variables.

Note: A Token based authorizer only gives you a small amount of event information compared to a Request based authoriser which is shown below, and does impact your decision on auth.

The event on a Token based authorizer looks like this below, with only a subset of information which most probably won’t suit our needs if we are doing any level of bespoke authorisation:


For a Lambda authorizer of the REQUEST type, API Gateway passes request parameters to the authorizer Lambda function as part of the event object.

The request parameters include headers, path parameters, query string parameters, stage variables, and some of request context variables.

"type": "REQUEST",
"methodArn": "arn:aws:execute-api:us-east-1:123456789012:s4x3opwd6i/test/GET/request",
"resource": "/request",
"path": "/request",
"httpMethod": "GET",
"headers": {
"X-AMZ-Date": "20170718T062915Z",
"Accept": "*/*",
"HeaderAuth1": "headerValue1",
"CloudFront-Viewer-Country": "US",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Is-Mobile-Viewer": "false",
"User-Agent": "...",
"X-Forwarded-Proto": "https",
"CloudFront-Is-SmartTV-Viewer": "false",
"Host": "",
"Accept-Encoding": "gzip, deflate",
"X-Forwarded-Port": "443",
"X-Amzn-Trace-Id": "...",
"Via": " (CloudFront)",
"X-Amz-Cf-Id": "...",
"X-Forwarded-For": "..., ...",
"Postman-Token": "...",
"cache-control": "no-cache",
"CloudFront-Is-Desktop-Viewer": "true",
"Content-Type": "application/x-www-form-urlencoded"
"queryStringParameters": {
"QueryString1": "queryValue1"
"pathParameters": {},
"stageVariables": {
"StageVar1": "stageValue1"
"requestContext": {
"path": "/request",
"accountId": "123456789012",
"resourceId": "05c7jb",
"stage": "test",
"requestId": "...",
"identity": {
"apiKey": "...",
"sourceIp": "..."
"resourcePath": "/request",
"httpMethod": "GET",
"apiId": "s4x3opwd6i"

Note: When using a Request based authorizer over a Token based it has a direct impact on your caching strategy, as you are essentially caching based on the various resource requests.

For more information please view the follow link. Now let’s cover the various options below.

Cognito User Pool Authentication + Lambda Authorisation ✔️

In this scenario we use the built in Cognito Authentication in API Gateway which simply validates the token against a Cognito User Pool.

“is the consumer of the API who they say they are?”

This allows us to use API Gateway’s built in User Pool Authentication, leaving the actual Authorisation to the consuming backing Lambda. This is shown in the diagram below:

Advantages ✔️

  1. This authentication is baked into Amazon API Gateway and AWS AppSync, so very easy to setup through IaC tools (e.g. CDK).
  2. We can use middleware alongside Lambda Layers to easily add the authorisation to the backing Lambdas which require it; which is very easy to setup and reuse.
  3. Changes in authorisation can be kept realtime if we remove the need for caching at the service layer levels. This means if we downgrade user permissions then this takes effect almost in realtime, as opposed to being tied into the authorisation and TTL of a cached token.


  1. Additional user context for authorisation needs to be hydrated and validated at the consuming Lambda Layer level which means it is not cached.
  2. To get a level of caching here we need to use a strategy such as API Gateway Caching on the ‘permissions’ service API, or Amazon DAX at the data layer of the ‘permissions’ service i.e. the service we hit to get additional user context, say the companies they are part of and their role based on token ID.
  3. If we use direct integrations over Lambda proxy integrations then this option is not applicable i.e. there is no consuming Lambda in this scenario to perform the authorisation through middleware/Lambda Layers. This is a major design consideration.
  4. Additional latency is hit every time when invoking the Lambda as this is where the hydration of user context happens (albeit we may want this realtime, and we can add caching at the service layer level if needed).

Note: We can use a Pre Token Generation Lambda in Cognito to add additional user context to the ID token (not Access Token) which is great for front end applications. We will cover the code for this later.

Lambda Authorizer Authentication & Authorisation ✔️

In this scenario we use a Lambda Custom Authoriser which moves both the Authentication and Authorisation to a specific Lambda which is invoked each time somebody hits the endpoints.

This does however allow us to set a cache on the access tokens/IAM Policy, meaning that a call to hydrate the token with additional context only happens once in the caching period.

Advantages ✔️

  1. This means that we can use this approach for direct integrations i.e. the custom authoriser is invoked at the API level, so integrations after this are unaffected and don’t require authorisation or authentication. This is a major design consideration.
  2. There is no need to perform authorisation at the consuming backing Lambda service layer level, which means they can be kept simple and not affected by auth changes.
  3. We can cache the auth at the API level so no need to do this at a service layer level (i.e. no need for services such as DAX). Once the token is generated with the correct permissions we cache the token for a period of time alongside the IAM Policy Document.
  4. The user permissions context is only retrieved from the permissions service once per cached token/IAM policy document.
  5. Authorisation and authentication logic is in one place.


  1. This adds additional complexity into one specific Lambda i.e. all of your authentication and authorisation business logic is in one place, meaning an issue or bug would affect all endpoints.
  2. This is a lot more complex to setup compared to a Cognito user pool authentication strategy, and needs a lot of custom code.
  3. The caching strategy becomes fairly complex, as user permissions are essentially cached alongside the token for a period of time (as this is where the permissions logic is), meaning that amending a users permissions does not take into effect until the TTL on the token is up, as the authorisation is baked into the custom authoriser. (an example would be caching the token for 5 minutes means that if you downgrade a user from a Manager to a Standard user they will still have Manager access for up to 5 minutes).
  4. We can hit the cold start time of the custom authoriser as well as a lower level service Lambda cold start, doubling the potential of overall latency.

Lambda Authorizer Authentication + Lambda Authorisation ✔️

In this scenario we use a Lambda Authoriser to authenticate the access token when somebody hits the end points, and we hydrate the user permissions for authorisation once when we cache the token in the Lambda layer (passing the additional user context through to the Lambda event of the backing Lambda)

Advantages ✔️

  1. We have a level of caching in this strategy when it comes to the users access token and hydrating the context once (one call to the permissions service when the token is generated in the Lambda Authorizer), with the context being passed through to the underlying consuming Lambdas to validate using middleware.
  2. The custom authoriser logic is more simple as it does not contain the business logic for validating permissions i.e. authorisation, it only contains logic for authentication and retrieving the permissions via an API.
  3. We can utilise the Context object of the Lambda event in the Lambda Authorizer to pass through the user permissions to the authorisation logic which lives in the service layer Lambda i.e. the consuming Lambda.


  1. If we use direct integrations over Lambda proxy integrations then this option is not applicable too i.e. there is no Lambda in this scenario to perform the authorisation through middleware of the permissions passed through in the event context. This is a major consideration.
  2. If your authorisation logic changes then you need to potentially update/redeploy many resources (rather than just redeploying a Custom Lambda Authorizer)
  3. We can hit the cold start time of the custom authoriser as well as a lower level service Lambda cold start, doubling the potential of overall latency.

Note: The default TTL value of the token cache in a Lambda Authorizer is 300 seconds. The maximum value is 3600 seconds (1 hour); this limit cannot be increased.

What are we building? 🏗️

We are going to build a solution which showcases two of the options allowing us to talk through the main code, which are:

  1. On the left hand side (purple numbers) which uses Lambda Authorizers for both authentication and authorisation.
  2. In the middle and right hand side (green and blue numbers) we cover using the built in Cognito authorizer for authentication, and performing authorisation at the lambda proxy integration level using Lambda Layers and Middy.

Let’s look at the services used in the next section before performing a deeper dive into the code and testing the solution.

Services Used 🧰

The following services have been used in this example:

✔️ AWS Lambda Layers

Lambda Layers provide a convenient way to package libraries and other dependencies that you can use with your Lambda functions. Using layers reduces the size of uploaded deployment archives and makes it faster to deploy your code.

✔️ Amazon Cognito

Amazon Cognito lets you add user sign-up, sign-in, and access control to your web and mobile apps quickly and easily. Amazon Cognito scales to millions of users and supports sign-in with social identity providers, such as Apple, Facebook, Google, and Amazon, and enterprise identity providers via SAML 2.0 and OpenID Connect.

✔️ Amazon API Gateway

Amazon API Gateway is a fully managed service that makes it easy for developers to create, publish, maintain, monitor, and secure APIs at any scale. APIs act as the “front door” for applications to access data, business logic, or functionality from your backend services.

✔️ Middy

Middy is a very simple middleware engine that allows you to simplify your AWS Lambda code when using Node.js.

Deploying the solution 🏗️

⚠️ Note: You will be charged for deploying this solution.

We can deploy the solution by performing the following steps:

cd infra && npm run deploy
cd external && npm run deploy
cd internal && npm run deploy
cd vouchers && npm run deploy

This will first deploy the infra stack which includes the user permissions service (i.e. the API we want to consume to retrieve permissions for users), and then the stacks for external consumers (machine to machine), and then users. We finally deploy the voucher service which is backed with full Lambda Authorizer authZ and authN.

⚠️ Note: you can remove the stacks using the equivalent ‘npm run remove’ script in the reverse order.

Testing the solution 🎯

We can test the solution using the Postman file found in the following folder:

./postman/Serverless Authorisation.postman_collection.json

Firstly, we need to generate the correct tokens.

M2M Auth

For machine to machine auth we use the built in OAuth2 client credentials flow which is baked into Postman, which will generate and cache our access tokens, as shown below (Client ID and Client secret can be retrieved from Cognito for the App Client):

User Auth

To use the Postman collection for a User we must first obtain a user access token through creating and logging in as a user through Cognito, and then logging in as shown below for ‘App client: UserOrdersClient’ and clicking on ‘View Hosted UI’:

This will redirect with a long string in the URL which contains both the access token and id token:


We can see when we log in we get both the ID and Access Tokens returned in the URL.

Access tokens are what the OAuth client uses to make requests to an API. The access token is meant to be read and validated by the API. An ID token contains information about what happened when a user authenticated, and is intended to be read by the OAuth client. The ID token may also contain information about the user such as their name or email address, although that is not a requirement of an ID token. —

Our ID token decoded using looks like this:

"at_hash": "yIG13eL_JoIm9srdWTDkQg",
"sub": "be16ec31-1f9e-4670-af9b-e42a81795482",
"role": "Manager",

"email_verified": true,
"iss": "",
"cognito:username": "be16ec31-1f9e-4670-af9b-e42a81795482",
"given_name": "Lee",
"aud": "p4iondtoa9aljeoultesa4nvc",
"companies": "[\"1111\"]",
"event_id": "46b6a22f-0455-4fb6-9623-69dd0f8e2ba1",
"token_use": "id",
"auth_time": 1658382584,
"phone_number": "+447512345678",
"id": "be16ec31-1f9e-4670-af9b-e42a81795482",
"exp": 1658386184,
"iat": 1658382584,
"family_name": "Gilmore",
"jti": "b8c27269-06a5-4d7f-a961-27dab149a16f",
"email": ""

Note: you can see that using a Pre Token Generation Lambda we have added additional data to the ID token for properties role, companies and id. These are additional claims through hitting our user permissions service when then token was generated, and can be used on the client apps. You will only see this once you seed the data in the next step.

Our Access token decoded using looks like this:

"sub": "be16ec31-1f9e-4670-af9b-e42a81795482",
"iss": "",
"version": 2,
"client_id": "p4iondtoa9aljeoultesa4nvc",
"event_id": "46b6a22f-0455-4fb6-9623-69dd0f8e2ba1",
"token_use": "access",
"scope": "orders/get.order orders/create.order phone openid orders/list.orders profile email",
"auth_time": 1658382584,
"exp": 1658386184,
"iat": 1658382586,
"jti": "e5ef65ee-23ad-42a9-ae83-a7213d68158b",
"username": "be16ec31-1f9e-4670-af9b-e42a81795482"

Note: it is the access token which we send with our requests in the ‘Authorization’ header as a bearer token.

Seeding permissions

Secondly, we need to go into the Permissions DynamoDB table and add the following entry with the id property being the User ID of the user created in Cognito. This is shown below:

and added to the Permissions table as so:

"id": "<your-user-id-from-cognito>",
"companies": ["1111"],
"role": "Manager"

Talking through key code 💬

Now let’s talk through some of the main code in the demo.

✔️ Adding context to the ID token through Pre Generation

We are able to add additional context to the ID token any time a user logs into Cognito. This is shown in the code below where we add this to the User Pool in the CDK under lambdaTriggers:

We can see that the Lambda code for this trigger hydrates the data and adds it to the token through the response:

This then allows us to utilise the additional user information which is passed through in the ID token in front end applications etc

✔️ Cognito User Pool Authentication + Lambda Authorisation

We can add the Cognito UserPool Authentication to our API endpoints as shown below in the CDK:

This authenticates the access token, but does no authorisation. The authorisation happens in the consuming/backing Lambda using Lambda Layers and Middy middleware.

We can create the Lambda Layer using the following code in the CDK:

The Lambda Layer points to the Validation folder which contains various Middy middleware functions. The first middleware hydrate-context.ts uses the users sub on their access token (their unique ID)and performs a lookup on the permissions API to hydrate the users permissions for the next middleware functions:

We then pass the user permissions to the validate-company-access.ts middleware which is shown below:

This takes the companies which the user has access to and checks the resource URL to ensure that the companies resource in the request is in the list of companies they have access too. It also checks that the user has the correct role for the specific endpoint too based on their permissions.

This is utilised in the Lambda handler code from the Lambda Layer as shown below:

This is shown in the following lines where we a.) hydrate the permissions b.) we check that the user has access to their own orders c.) we check that the user has access to the company as a manager role:

export const getOrder = middy(handler)
.use(hydrateContext()) // get the permissions for the user

In this approach we have the authentication in the Lambda Authorizer and the authorisation happens in the consuming/backing Lambda function through middleware.

✔️ Lambda Authorizer Authentication & Authorisation

In this approach we perform both the authentication and the authorisation in the Lambda Authorizer, and the consuming Lambda has no auth logic at all. An example of how this could be done is shown below:

This approach has the main benefit of allowing us to utilise direct integrations of services from API Gateway.

✔️ Lambda Authorizer Authentication + Lambda Authorisation

Just for a note on this, we could have done this approach using a mix of the methods and code above.

You can see in the file vouchers/src/auth-handler/auth-handler.ts that we pass through the user permissions context using the following code:

const policyDoc = generatePolicy("Allow", "*");
return {
principalId: decodedToken.sub,
policyDocument: policyDoc,
context: {
companies: JSON.stringify(permissions.companies),
role: permissions.role,

} as AuthResponse;

If we removed the actual permissions logic from this auth-handler then we could have then used the middleware approach (discussed above) to perform the validation at the backing Lambda layer (utilising the cached context passed through in the event).

Note: You will see above that we have added a ‘*’ into the generatePolicy method which essentially means allow all endpoints if the permissions are validated (i.e. they have access to the company as a ‘Manager’). We could have changed this to event.methodArn which would have only allowed that specific endpoint and method, however if a user performs a POST i.e. creating a resource, then you would expect that they would be able to PUT, GET, DELETE etc too — but those permissions would not allow it from the cached policy. This is an article in its own right, but I just wanted to touch upon this.

You could work around this issue above by storing the actual IAM policy documents in the permissions table also as discussed in this article:


With all things considered I would typically go for the approach of a custom Lambda Authorizer which performs both authorisation and authentication, allowing us to utilise direct integrations with downstream AWS services, as long as the authorisation logic is not too complex!

I hope you found that useful! In the next part we will cover Lambda Authorizers with AWS AppSync.

Wrapping up 👋

Please go and subscribe on my YouTube channel for similar content!

I would love to connect with you also on any of the following:

If you enjoyed the posts please follow my profile Lee James Gilmore for further posts/series, and don’t forget to connect and say Hi 👋

Please also use the ‘clap’ feature at the bottom of the post if you enjoyed it! (You can clap more than once!!)

About me

Hi, I’m Lee, an AWS Community Builder, Blogger, AWS certified cloud architect and Enterprise Serverless Architect based in the UK; currently working for City Electrical Factors (UK) & City Electric Supply (US), having worked primarily in full-stack JavaScript on AWS for the past 6 years.

I consider myself a serverless advocate with a love of all things AWS, innovation, software architecture and technology.

*** The information provided are my own personal views and I accept no responsibility on the use of the information. ***

You may also be interested in the following:



Principal Serverless Engineer | Enterprise Cloud Architect | Serverless Advocate | Mentor | Blogger | AWS x 7 Certified 🚀

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Lee James Gilmore

Principal Serverless Engineer | Enterprise Cloud Architect | Serverless Advocate | Mentor | Blogger | AWS x 7 Certified 🚀