Skip to main content

Command Palette

Search for a command to run...

How to Setup AWS Custom Lambda Auth for Auth0 — part 1

Published
10 min read
How to Setup AWS Custom Lambda Auth for Auth0 — part 1

Now if you already know all the terms in the title and have a fair understanding, you wouldn’t want me yapping about how to do it so TLDR, this is what we will be understanding below.

In this blog, we will focus on the AWS part of the above architecture

There already exists an excellent blog from Auth0 itself on how to do it and I highly recommend you to read it to increase the context for understanding for implementing this.

[Secure AWS API Gateway Endpoints Using Custom Authorizers
How to use secure AWS API Gateway using custom authorizers that accept Auth0-issued access tokens.auth0.com](https://auth0.com/docs/customize/integrations/aws/aws-api-gateway-custom-authorizers "https://auth0.com/docs/customize/integrations/aws/aws-api-gateway-custom-authorizers")

There are already several blogs that explain how to create a custom authorizer step by step, including the one mentioned above. However, this blog aims to provide a better understanding of the process and share some insights gained during the development process.
Let's start step by step here:

What even is Custom Lambda Authorizer anyway?

It was confusing in the start and just more confusing as it went on

So imagine you have a building in which you want only your trusty besties to have permission to enter. You can do it in a couple of different ways:

You would start by having some authentication or a person who checks if the person trying to enter is on the allowed list.
Now the question comes to mind, what would be a way to determine that?

For instance, a simple token such as an ID card can serve as a means of verification. It can be checked at the entry point or the person can provide some kind of information to prove their eligibility to access the building.

The Custom Lambda Authorizer does just that, it exists to protect/access control the API routes which you have exposed to the internet through API Gateway. Of course, there are a couple of other things you can do to protect them like setting up CORS if you already know who will use that API.

Lore about Lambda Authorizer!?

There are 2 ways in which you can do it, one is using the simple JWT validation which will check the issuer(who issued the token) and audience (who is the final audience or consumer of that token) field to see if it is good to go or not, it's easy to implement and not flexible for complex cases.

Source from docs.aws.amazon.com

The second type is the custom Lambda Authorizer where you can connect a Lambda function to do all the processing and either return a boolean(true or false) value or an IAM policy. This opens up many possibilities for complex use cases.

The custom Lambda Auth also comes in 2 types:

Token Based

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.

In essence, you specify the name of the header which contains the token generally it is Authorizationand the API Gateway passes the value of that header to the authorizer where you can authenticate.

The event of your token-based authorizer looks something like this

{
"type": "TOKEN",
"authorizationToken": "allow",
"methodArn": "arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/"
}

Request Based

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.

You can use Request parameter-based Lambda when you want more information about the request to implement more fine-grained access control.

If you are feeling zesty you can apply tech-racism(geo-fencing) to only allow requests from a certain Location/IP.

One other use case which comes to my mind is that suppose you have purchasing power parity pricing for your products ie prices according to the economical condition of the country then you can use request-based lambda to block requests if the person is trying to use the service from a location different from their buying country.

The event of your request-based authorizer looks something like this

{
"type": "REQUEST",
"methodArn": "arn:aws:execute-api:us-east-1:123456789012:abcdef123/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": "..."
},
"queryStringParameters": {
"QueryString1": "queryValue1"
},
"pathParameters": {},
"stageVariables": {
"StageVar1": "stageValue1"
},
"requestContext": {
"path": "/request",
"accountId": "123456789012",
"resourceId": "05c7jb",
"stage": "test",
"requestId": "...",
"identity": {
"apiKey": "...",
"sourceIp": "...",
"clientCert": {
"clientCertPem": "CERT_CONTENT",
"subjectDN": "www.example.com",
"issuerDN": "Example issuer",
"serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1",
"validity": {
"notBefore": "May 28 12:30:02 2019 GMT",
"notAfter": "Aug 5 09:36:04 2021 GMT"
}
}
},
"resourcePath": "/request",
"httpMethod": "GET",
"apiId": "abcdef123"
}
}

As you can see, the request-based authorizer is information-rich and you can use that to your advantage.

Additionally, you can know the type of your lambda auth by:

export const handler = async (event, context) => {
const {type}=event;
console.log("Type: ",type);
/**
here the type would contain the type of your lambda Auth and you can
change the type by the AWS CLI although funnily enough the type is
not asked when creating the Authorizer in AWS Console.
docs.aws.amazon.com/cli/latest/reference/apigateway/update-authorizer.html
**/
};

At the start, I thought I had a TOKEN-based authorizer but it was a request one 😅

Before we start, I highly recommend you to connect your API Gateway and your Authorization Lambda function to Cloud Watch for logging(All the stats, console logs and additional information will be available in Cloudwatch). It will save you tons of time instead of shooting your arrow in the dark while debugging(cause you will heavily need it, trust me). The logs in Cloudwatch can take up to 1 minute to update whenever an event happens. The log format can be like this but you can change it as you see fit. Here the authorizerError variable will give you a hint on why the authorizer rejected your request.

{
"requestId":"$context.requestId",
"ip":"$context.identity.sourceIp",
"requestTime":"$context.requestTime",
"httpMethod":"$context.httpMethod",
"routeKey":"$context.routeKey",
"status":"$context.status",
"protocol":"$context.protocol",
"responseLength":"$context.responseLength",
"errorMessage":"$context.error.message",
"errorType":"$context.error.responseType",
"authorizerError":"$context.authorizer.error"
}

How does this work anyway?

Trust me that diagram only looks complicated

Right now I will be discussing the custom lambda authorizer one only as others are out of scope for this blog(and simple enough to implement). Typically an authorizer works in the following way as given in the docs of AWS

from docs.aws.amazon.com explaining the working

In my specific scenario, I needed to verify whether the token added in the Authorization field of the request header is a legitimate Auth0 JWT Token. Although this could have been achieved using JWT-type authorization, I designed the lambda with future adaptability in mind, to allow implementation of additional features.
if you have already read the Auth0 blog which I mentioned earlier then you would have an idea that you can make permission or custom scopes while making the Auth0 M2M API for the token generation cause you can use that in the Lambda after decoding the token. The general handler function is like this

export const handler = async (event, context) => {
try {
const policy = await authenticate(event);
context.succeed(policy);
} catch (e) {
console.error("Error->\n", e);
context.fail("Unauthorized");
}
};

and in the authenticate function you can do all the processing or apply validation rules.

In my implementation, I first check if there is an authorization field in the header or if the token is present in the proper format(Bearer ) and throw an error if any check fails. If everything is A-OK then return the token.

If you are using Node Runtime for your Lambda then you can use Jose library for all the JWT-related operations. If you are using other runtimes, then you explore the libraries on jwt.io
When using an external library like Jose in a Lambda function, it's necessary to create a custom layer. A custom layer is a zip file containing the package.json , package-lock.json and node_modules folder which is required.

I fetched the JSON Web Key Set(JWKS), which contains the public keys used to verify any JSON Web Token (JWT) issued by the Authorization Server and signed using the RS256 signing algorithm.

Auth0 exposes a JWKS endpoint for each tenant, which is found at https://{yourDomain}/.well-known/jwks.json. This endpoint will contain the JWK used to verify all Auth0-issued JWTs for your tenant. I am pretty sure that this endpoint is rate-limited, this is a hint which will be useful later because this messed with me so badly.

After obtaining the token, I decode the token and verify if the issuer and the audience are correct or not, if everything is ok and the token is safely decoded then an Identity and Access Management (IAM) Policy is made which will be returned.
For the IAM Policy, you can make a function like this to generate a generic policy

const getPolicyDocument = (effect, resource) => ({
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke", // <-- What action they can take.
Effect: effect, // <-- Allow/Deny the action
Resource: resource, // <-- The resource on which they can perform
// this action
},
],
});

{
...
return {
principalId: user,
policyDocument: getPolicyDocument("Allow", params.routeArn),
context: { scope: payload.scope },
};
}

A Lambda authorizer function’s output is a key-value pair object, which must include the principal identifier (principalId) and a policy document (policyDocument) containing a list of policy statements. The output can also include a context map containing key-value pairs.

PrincipalId The principalId is a required property on your authorizer response. It represents the principal identifier for the caller. In my implementation, I set principalId as the JWT payload.sub which is essentially the Client ID of the Auth0 Application. This is just used for identification so don’t worry too much about it.

PolicyDocument The policyDocument contains the IAM Policy to pass back to the API Gateway. Writing a policy document is as tedious as it gets so if you are a beginner, just copy it. The basic essence of a policy document is that it contains 3 main things: Action ,Effectand Resource.

Action tells what action can be taken. Effect dictates if the policy should allow or deny access to the resources described in Resource. The resource contains the ARN of the resource.

Context This is the optional key which you can use to pass additional information to the integration. You cannot set a JSON object or array as a valid value of any key in the context map. You can pass the information from the token to the integration if needed, using the context.

This is all the additional context you would need to understand the AWS side of things for a custom Lambda Authorizer.
But just when I thought that I had finally written and deployed the API, the testing had me in tears.

It took some time, trial & error and lots of console.logs to recognize and completely integrate the authorizer with every route that I had to protect. One piece of advice I want to share is to create separate authorizers for each route, even if the same Lambda function is connected to each one.

When in doubt, just console your logs…

For me, everything was done but still sometimes out of the blue, there was an UNAUTHORIZED error from the authorizer and after some time it started working again. It was one of the most confusing things I have encountered cause it happened on its own and fixed on its own too. It was only much later that I found out that it might be happening due to fetching the JWKS from that endpoint.

I often message my brother about my progress just to keep a log :P

It was then I found out that it was happening due to rate-limiting and I had to implement a cache strategy in my Lambda for the keys.

In the second part, I will be talking about the front-end and Auth0 parts and how I almost auditioned for a circus because of my stupidity.

[How to Setup AWS Custom Lambda Auth for Auth0 — part 2
Secure your API Gateway routes with a custom Lambda authorizer for Auth0-issued access tokens. This is the second part…medium.com](https://medium.com/@louremipsum/how-to-setup-aws-custom-lambda-auth-for-auth0-part-2-fe3e527ee19c "https://medium.com/@louremipsum/how-to-setup-aws-custom-lambda-auth-for-auth0-part-2-fe3e527ee19c")

Follow me on Twitter for regular updates.

またね / matane / फिर मिलते है

Additional Resources which helped me

[Use API Gateway Lambda authorizers
Enable an Amazon API Gateway Lambda authorizer to authenticate API requests.docs.aws.amazon.com](https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html "https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html")

[**Highly Recommended: The Complete Guide to Custom Authorizers with AWS Lambda and API Gateway | DeBrie Advisory
API Gateway custom authorizers are a great way to separate auth logic from business logic in serverless applications…www.alexdebrie.com](https://www.alexdebrie.com/posts/lambda-custom-authorizers/ "https://www.alexdebrie.com/posts/lambda-custom-authorizers/")

[Input to an Amazon API Gateway Lambda authorizer
Learn the format of input to a Lambda authorizerdocs.aws.amazon.com](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html "https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html")