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

Vinayak Nigam
10 min readMar 19, 2024

This is the second part of setting up AWS Custom Lambda with Auth0. In this blog, we will see the frontend part which people generally miss and also the mistakes which I made as a first-timer.

To read the first part, visit my profile or

To fully understand this blog, I urge you to also open the links which I am embedded in the blog to get the full context. These links can be identified by underlined words like this one.

Remember how last time I told you that API Gateway strips the header which contains the ID Proof? This happens by setting the Identity sources in the configuration of the authorizer. By default that is $request.header.Authorization so in your “frontend” or more typically backend of your web application, you put an additional header field by the name of Authorization and set its value as the token value(I will be giving the example later).

For my use case, I started my project with Vite and did the initial core functionality in that only but when I wanted to implement this authorizer functionality I had to append the access token to the request and that was visible in the network tab in the developer console. So not only was I leaking the URL of the API endpoint but also the access token which is a big no-no. Truth be told, the generation of access tokens and sending them somewhere securely is almost impossible to do in a SPA. So I had to shift my whole project from Vite to a framework with frontend and backend. I chose NEXT JS cause I always wanted to give it a try and staying in the trend of hitting myself in the balls even more, I chose the fabled App Router configuration. Now there is nothing wrong in choosing the app router Per se but since it is so new, it is hard to find documentation for it and you would have to dig in actual function definitions and incomplete docs to write it. To tell you a scale of how new it is, even GPT and Co-pilot did not know about Nextjs 14.

We will start at the API part directly since that is the main focus, I will be writing about my experience with Nextjs 14 app router in another blog.

I will first talk about my initial thought process and hopefully, you will be able to find out the mistake in it. I will also be talking about how to fix it.
For the initial part the process is pretty simple, fetch the token from Auth0 and then append it to Authorization field in the header. Simple right?

Well… Not so much, let's take it step by step

But what is a token?

I have been talking about sending a token all this time but what kind of token and which token? Tokens in this context are the JSON Web Tokens which are encrypted pieces of information which contain certain information.

Hein? JSON Web Token? We have extensively heard about JSON but what are those other big words?

JSON Web Tokens are used in the industry more and more. The spec which defines them (RFC7519) describes them as a compact, URL-safe means of representing claims between parties by encoding them as JSON objects which can be digitally signed or encrypted.

What this means is that they are a claim(information) which can be used to act as a pass between systems to know if the system is actually who it says it is. It contains 3 things(denoted by their colour in the below image): Header, Payload and Signature. The Header tells about the token like what type of token it is and what algorithm was used to sign it. The payload contains the actual information. The JSON properties in the payload are called claims, and they are declarations about the user and the token itself. The claims about the user define the user’s identity. The signature is there for verification if the token is valid or not.
I won’t go into much detail about them but there are 3 fields in the payload which we will be using here and you should know about like aud , exp and sub .

This is an example of an RS265 signed token:

go to jwt.io for more information

We will only be talking about RS256 encrypted tokens. Auth0 also provides an HS256 signing algorithm.

Mainly there are 2 types of tokens which Auth0 or a service provides: ID Token and Access Token.

Although there are many differences in the said tokens and I would highly recommend you to read about them from the link, I will summarize them as follows:
ID Token is the token which proves that the user is authenticated so it contains information identifying the user. An example would be authenticating an app using an identity provider like Google(Login with Google button). That identity provider will give you an ID Token to prove your identity and that token will be given to the web app.

Access Token is like a token that the user approves by authenticating and you can use that token to do actions on behalf of the user, kind of like if you use typefully to post tweets from it, you would have to authenticate on Twitter which would give an access token to typefully to post tweets on your behalf.

This beautiful diagram from the Auth0 blog tells the difference in a cool manner(I want to give my heartfelt compliments to whoever designed this)

Credits: Auth0 Blog

The reason why I invested so much time in telling you this is that I don't want you to just copy code and make it work but also give you an idea of how awesome of a technology you are building and how it all works because this context will surely help you in the future.

As you might have guessed, we will be using access tokens for our operations to call the API. Now getting an access token from Auth0 is kind of simple and I will not be going into much detail. Getting an access token for management API is kind of tedious but still not that difficult. You just have to make an M2M API from Auth0 and use that audience for the AWS Lambda Authorizer as you will be checking that too.

At first, I had a simple enough implementation for either getting a simple access token for AWS or Auth0 Management API.

const createAccessToken = async (manageAPI: boolean) => {
const options = {
method: "POST",
url: process.env.URL_AT,
headers: { "content-type": "application/x-www-form-urlencoded" },
data: new URLSearchParams({
client_id: manageAPI
? process.env.AUTH0_ACCOUNT_MANAGE_CLIENT_ID!
: process.env.AUTH0_CLIENT_ID!,
client_secret: manageAPI
? process.env.AUTH0_ACCOUNT_MANAGE_CLIENT_SECRET!
: process.env.AUTH0_CLIENT_SECRET!,
audience: manageAPI
? process.env.AUTH0_AUD_MANAGE!
: process.env.AUTH0_AUDIENCE!,
grant_type: "client_credentials",
}),
};

try {
const response: AxiosResponse = await axios(options);
return response.data;
} catch (error) {
console.error(error);
}
};

const token = await createAccessToken(false);
const res = await axios.post(process.env.CREATE_QR!, formWithUser, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token.access_token}`,
},
});

As an alternative to making HTTP calls, you can use the node-auth0 library to automatically obtain tokens for the Management API.

Now note that getting an M2M access token is rate-limited ie only 1000 M2M tokens per month only. And if you didn't guess now, the above functions get a new access token for each request to API Gateway. I got an email from Auth0 one fine morning:

huh, that’s weird. Must be a mistake

And then I opened the quota utilization of Auth0

Yeah I was casually sitting with a 150 BPM

Then I finally realised my stupidity. And finally read the token best practices page from Auth0, specifically the store and re-use one.

I then started researching how to securely store a token. This also turned out to be a huge debate, because just like everything under the sun in web development, there are tons of ways to do it and everyone is saying their method is right. The main moto is that you have to cache the token somewhere secure from where you can retrieve it and use it.

For Traditional web applications, Auth0 says that: If your app needs to call APIs on behalf of the user, access tokens and (optionally) refresh tokens are needed. These can be stored server-side or in a session cookie. The cookie needs to be encrypted and have a maximum size of 4 KB. If the data to be stored is large, storing tokens in the session cookie is not a viable option.

There are many ways to cache data or maybe your framework of choice also gives you this option in some way so doesn’t matter how you do it. For my implementation, I used Redis for caching the tokens and encrypting the data at rest in Redis. Maybe it is not the most secure option but I think it is still viable. For Redis, you can use Upstash-redis or just normal Redis cloud, both of them are viable for your use-case in case you wanna store the token there. I did decrease my token usage to 1 per day tho.

This brings me to the second part of the blog, actually implementing it.

The Backend part logic
/**
* Creates an access token using the provided manageAPI flag.
* @param manageAPI - A boolean flag indicating whether the access token is
* for Management API or not.
* @returns A Promise that resolves to the access token data.
*/
const createAccessToken = async (
manageAPI: boolean,
retryCount = 0
): Promise<string> => {
try {
/** When you use fetch inside a server action file then it needs to
* have the full link of the api
*/
const response = await fetch(
`${process.env.WEBSITE_URL}/api/accessToken?manageAPI=${manageAPI}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
"x-secret-key": process.env.INTERNAL_SECRET_KEY!,
},
cache: "no-store", // This is Next 14 feature to not cache as
// fetch req are auto cached in next 14.
}
);
const token = await response.json();
const exp = getExpiration(token.accessToken);
if (Date.now() <= exp) {
if (retryCount >= 3) {
throw new Error("Token refresh limit exceeded");
}
return await createAccessToken(manageAPI, retryCount + 1);
} else {
return token.accessToken;
}
} catch (error) {
return (error as Error).message;
}
};

The logic is simple enough, whenever a token is needed, hit the internal API route for access token. The function will check if the token for that audience is in the cache or not, if it is then it will retrieve the token and check if the token is expired or not. If the token is expired then get a new token from Auth0 URL and store it in the cache. This token will then be returned. If the token is not expired then the token will be returned directly.

In my logic for creating the token, I have been extra cautious to check for the expiry of the token there also and retry up to 3 times before throwing an error.

// token.ts

function getExpiration(token: string): number {
const { exp } = decodeJwt(token);
return exp || Math.floor(Date.now() / 1000);
}

export async function getAuth0M2MTokenWithCache(
manageAPI: boolean
): Promise<string> {
const audience = manageAPI
? process.env.AUTH0_AUD_MANAGE!
: process.env.AUTH0_AWS_AUDIENCE!;
try {
const tokenFromCache = await redis.get<string>(audience);
if (tokenFromCache) {
console.log("Token recovered from cache");
const exp = getExpiration(tokenFromCache);
if (Date.now() >= exp) {
return tokenFromCache;
}
}
} catch (error) {
console.log("There was an error recovering token from cache");
if (error instanceof Error) {
console.error(error.message, error);
}
}

const accessToken = await getAuth0M2MToken(manageAPI);
// getAuth0M2MToken is the same function from earlier(createAccessToken)
console.log("Generated new access token");
try {
await redis.set(audience, accessToken, {
exat: getExpiration(accessToken),
});
console.log("Stored access token in DB");
} catch (error) {
console.log("There was an error storing token on cache");
if (error instanceof Error) {
console.error(error.message, error);
}
}

return accessToken;
}

Of course, you can just do it all internally and there is no need to make an API endpoint for this. I did this because, in future, I will also be implementing rate-limiting on this API so that it cannot be abused and some security features.

Hopefully, my intent to explain has reached you about how to integrate Lambda Authorizer and form requests in the backend.
If you have any questions about the process, feel free to comment or DM/email me and I would be happy to help you to the best of my ability.

Maybe sometime in the future, I will write about my experience of using Next JS App router as someone who has used Next for the first time straight from React. Although it might take time for the research.

Follow me on Twitter for regular updates and thank you for reading.

--

--

Vinayak Nigam

I'm Vinayak aka louremipsum - Frontend Developer, UI/UX designer, and problem-solving enthusiast. Passionate about coding, anime, philosophy, and random things