How to protect APIs with JWT and API Gateway Lambda Authorizer

Mariano Calandra
Oct 16 · 8 min read

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a way for transmitting information – like authentication and authorization facts – between two parties: an issuer and an audience. Communication is safe because each token issued is digitally signed, so a consumer can verify if the token is authentic or has been forged.

In a previous story, we talk in depth about it: how it is built, what problems it solves, what is the theory behind signature’s validation and, finally, how we can protect our resource.

Fig. 1 — The classic flow of a serverless authentication and authorization.

In that scenario, a client who wants to access a protected resource needs a JWT. Without it, the same client needs to contact the authentication server exchanging username and password for a brand new JWT (from steps 1 to 3 of Fig.1). With this token, it can eventually request the protected resource trough Amazon API Gateway, that has to validate it (from steps 4 to 6 of Fig.1).

By that article, we presented the theory but nothing has been said about a real implementation.

We need to fix it!
But first… a step back!


Framing the context

Amazon API Gateway is a wonderful tool with many features, but at its core, it is basically a component that routes an incoming HTTP request to the responsible backend.

Fig.2 — A classic API REST on AWS.

The image above well represent this concept, as we have:

  • the HTTP REST endpoint: implemented using Amazon API Gateway (e.g. GET /orders);
  • the endpoint’s backend: it could be an AWS Lambda function (like in Fig.2), a containerized microservice, a load balancer, an HTTP endpoint and so on;

So, what about token’s validation?

As far as we have seen, cowboy’s logic would suggest to introduce validation code right into the backend logic. Something like:

I don’t really like to mix authorization and business code together but, especially in a monolithic application, there’s nothing intrinsically bad with this approach. Problems starts when we have a more service oriented architecture, like microservices.
If every service would be a whole with authorization code, as soon as we found a bug, we have to redeploy the entire system just to fix it.

Fig.3 (don’t try this at home) — A microservice system strongly coupled with authentication logic.

A more savvy approach, would suggest to eradicate the authorization logic from the business code, creating a new service dedicated to it.
The day we need to fix the authorization logic we have to redeploy just one service type.

Fig.4 — A microservice system with an external service for authentication logic.

Back to the Fig. 2 example, now we know it’s better to split the direct communication between Amazon API Gateway and AWS Lambda, using an external service to handle authorization logic.

Introducing Lambda Authorizer

A Lambda Authorizer is a peculiar type of Lambda function. It accept an object containing a token and returns a JSON policy to allow or block an API execution. Something like this:

With the above policy, the API Gateway service (i.e. the principalId equals to apigateway.amazonaws.com) is allowed (i.e. Effect equals to Allow) to invoke (i.e. Action equals to execute-api:Invoke) a given API resource (e.g. Resource equals toarn:aws:execute-api:{REGION}:{ACCOUNT_ID}:{API_ID}/Prod/GET/).

In order to understand what policy our function have to return, it needs to verify the token. By convention the authorizer function accept an argument (in Lambda dialect its name is Event Source), henceforward referred to as event, that has two important properties:

  • event.authorizationToken — the token we want to verify;
  • event.methodArn— the ARN of the API request that triggered the authorizer;

Our first Lambda Authorizer

To gradually introduce the logic, here we present a very simple (and not so secure) authorizer:

In the above code, the authorizer evaluate the token, if its value is OK it will be returned a policy that will let the request flow to continue. Otherwise, the policy expressed by denyAllPolicy is returned and flow is interrupted.

Creating our first Lambda Authorizer

As first step we want to build the Lambda Authorizer itself, so create a Node.js 10.x Lambda function and paste the above code in the editor and save (here a tutorial).
Second step (optional if you already have an API to play with) is the creation of an API that we want to protect. To go as fastest as possible, we can create a new serverless Web Application — no matter the language — using AWS CodeStar (here a tutorial).

Once CodeStar have built all the components, open the Amazon API Gateway console and search for this new API (the name will be similar to the name of the CodeStar project) and expand it.

Fig.6–The authorizers list (currently empty) for our API.

At this point we can jump into “Authorizers” tab and click “Create New Authorizer”.

Fig.7–The creation of our first authorizer.

To create a new Authorizer we need to choose a name (e.g. lambda-authorizer-test) and a type (i.e. Lambda).

Note: Lambda Authorizer is a great tool when we need a custom authorization behaviour or when our users are stored outside of an Amazon Cognito User Pool, otherwise we can choose Cognito. Out of scope here.

Inside the Lambda Function field, we have to provide the Lambda function that will act as authorizer and that we’ve created as the first step of this section.

Finally, we have to explain how our client will send the token during API request. By convention this is sent through the Authorization HTTP header, so we can set it into the “Token Source” (Fig.7).

For this example we don’t want to set a regular expression to validate the syntactic correctness of the token, neither we want to use a cache, so we just hit “Create”.

Fig.8–API Gateway needs a role to execute the function, here we can create it.

Since Amazon API Gateway needs a role with specific permissions to invoke the authorizer, if the above popup appear, accept the hint by clicking “Grant & Create”.

In the “Authorizers” tab, previously empty (Fig.6), now we can see our Authorizer.

Fig. 9–Once we create our authorizer we can test it.

Before we go any further, click “Test” and check the it’s logic. In the example below, we use the string “NOT OK” that, as expected, returns the deny-all policy.

Fig.10–The test result of our authorizer.

Wire everything together

Now that we have everything in the right place, we can finally split the binds between Amazon API Gateway and the backend Lambda introducing the authorizer in the middle.

In the same Amazon API Gateway dashboard click the “Resources” menu item, this will show all the resources associated with this API. Select the HTTP verb that we wish to protect (if we followed the tutorial we only have the root resource / and GET verb).

Fig.11 — The API flow, from request to integration.

Click the “Method Request” and when we are ready, in the “Authorization” dropdown select the authorizer created before (in this case lambda-authorizer-test; if we can’t see it, it should be sufficient to refresh the page), then click the ✔ sign to confirm (Fig.11).

Fig.11–Here we can choose what is the authorizer that we want to use.

Deploy and test the modifications

At this point we have correctly configured the authorizer but we still need to deploy API to make things working. Click the dropdown “Actions” and then select “Deploy API”. A popup will appear, so we can choose the target stage in which we want to deploy our API (e.g. Stage) and then click “Deploy” (if you need more details, here is an official document by AWS).

If everything will go as expected we would see the “Invoke URL“. Copy that URL and make a request using a tool like Postman, so we can easily fill the Authorization header in the request (Fig.12).

Fig.12–The output that we get calling the protected API with a wrong token.

In this case we got a 403 Unauthorized error and an explicit message that says:

User is not authorized to access this resource with an explicit deny.

A real authorizer

As said before, the above authorizer is not so secure. We need something smarter, that will be capable of execute the validation steps defined in the previous story. These are:

  • Decode the token so we can get the JWK endpoint;
  • Invoke this endpoint and retrieve the exponent and modulus that compute the public key (using the kid to identify the right key to pick);
  • Compute the PEM;
  • Verify the token against the PEM;

If no error occurs during this steps an allowPolicy will be returned.

Here a pseudo code:

Even though we can still perform further checks this algorithm is much safer than the one seen before.

Summary

In the first article of this series we introduced JSON Web Token (JWT). We looked its characteristics and why it is so popular nowdays.

In this post, instead, we showcased how a JWT fits into the AWS ecosystem, especially in the Amazon API Gateway. Using this feature, we can split the communication between the HTTP endpoint (e.g. GET /orders) and its backend (e.g. GetOrders Lambda function) introducing a layer for authorization strategies.

We highlighted steps needed to create and test our first and trivial authorizer. If you want to see code for a complete example checkout this GitHub repo.

It seems really everything for now but we still miss a thing: a JWT!

How to get one? What services can I use?
We will discuss this in a third post. :)

The Startup

Medium's largest active publication, followed by +524K people. Follow to join our community.

Mariano Calandra

Written by

Mariano daily helps companies that wish to modernize their applications embracing microservices and cloud native architecture. He’s an AWS Authorized Instructor

The Startup

Medium's largest active publication, followed by +524K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade