Implementing OAuth 2.0 with AWS API Gateway, Lambda, DynamoDB, and KMS — Part 3

Bilal Ashfaq
8 min readAug 5, 2023

--

This is the third article in the series to implement OAuth 2.0 Client Credentials flow using AWS Serverless technologies. In the previous part, we created an authorization server and in this part, we will create a Resource Server API Gateway that would serve client requests.

Please check out Part 1 and Part 2 before continuing.

Before starting, let’s have a look at the architecture diagram for our Resource Server API Gateway again:

Resource Server — Architecture

As shown in the diagram, we first need to create a resource lambda and connect it with an API Gateway. Then, we need to create an authorizer lambda that will be attached to the API Gateway endpoint and it will validate the access token provided by the client. Only if the token is valid, will the client get access to the resource lambda.

Let’s start by creating the Lambda function that will return protected resources to the user:

Create Resource Lambda Function:

To create the lambda function, navigate to the Create Function page on the Lambda console.

Make the following selections: choose “Author from scratch”, type the name for your lambda function (getProducts), choose runtime Node.js 18.x, for Architecture choose x86_64,

leave the rest of the settings as default, and click on “Create function”

This lambda function is very simple and just returns a string as a response as shown in the code below:

export const handler = async(event, context) => {

console.log("Event ", event);
const response = {
statusCode: 200,
body: JSON.stringify('Protected Resource!'),
};
return response;
};

Create Resource Server API Gateway:

To create the API Gateway, follow the exact same steps from the previous article. The only difference here would be that the resource we create will be called “products” and we will create a Get method for that resource as shown in the image below:

Also, note that we are attaching the Resource Lambda we just created with this endpoint.

Now deploy the API and test it through Postman.

If we try to hit the resource endpoint through Postman now without providing any Access Token, it will give back the response because we have not attached any Authorizer Lambda to the endpoint yet.

So now, let’s go ahead and create our Authorizer lambda

Create Authorizer Lambda Function:

Let’s have a look at the code in each file/module of our authorizer lambda function one by one:

helpers.mjs

This module has the following functions,

  • generatePolicy: which will create the IAM policy to be returned by this lambda function.
export const generatePolicy = (clientId, effect,  scope) => {

let policy = {
principalId: clientId,
policyDocument: {
Version: "2012-10-17",
Statement: [
{
Action: "execute-api:Invoke",
Effect: effect,
Resource: "arn:aws:execute-api:us-east-1:<account-id>:<resource-server-api-id>/*/*"
}
]
}
};

if(scope){
policy.context = {
permissions: scope.join(" ")
}
}
return policy;
};

Notice that the “principalId” attribute in the policy is being set as the client Id. Also notice that, inside Statement, we are either allowing or denying API invoke action on all the endpoints of our Resource Server API Gateway.

Note: make sure that the arn of Resource Server API Gateway is set as the value of the “Resource” attribute in the policy statement

Finally, we are adding permissions attribute inside the context, and it will be available in the underlying (resource) lambda function. Then, we can use the permissions inside the resource lambda function to only allow the permitted actions.

  • getToken: this function will just get the Token from concatenated token string
    i.e. from “Bearer tokenString” it will get “tokenString”.
export const getToken = (tokenString) => {
let tokenParts = tokenString.split(" ");
if(!tokenParts || tokenParts.length != 2 || tokenParts[0] !== 'Bearer'){
throw new Error("Token is not valid");
}

return tokenParts[1];
};

publicKey.pem:

This is the public key for the key we used to sign our tokens. To get the public key, open the key we created on KMS, switch to the “Public Key” tab, and copy the key.

jwtVerificationHelper.mjs:

This module has the function verifyJWT that will use jsonwebtoken npm package to verify that the Access Token is valid.

import jwt from 'jsonwebtoken';
import fs from 'fs';

export const verifyJWT = async (token) => {
try{
let publicKey = fs.readFileSync('publicKey.pem');
let parsedToken = await jwt.verify(token, publicKey, { algorithms: 'RS256', issuer: 'https://<auth-server-api-id>.execute-api.us-east-1.amazonaws.com' });
console.log("Parsed Token ", parsedToken);
return {
isValid: true,
clientId: parsedToken.client_id,
scope: parsedToken.scope
}
}
catch(err){
console.error(err);
return {isValid: false};
}
}

First, this function reads the public key from publicKey.pem file and then calls verify function from jsonwebtoken module. The verify function takes the token, public key, and additional options object where we are providing valid algorithms and issuer (which is our Authorization server endpoint).

Note: issuer param should match the “iss” attribute of the access token, otherwise token would be considered invalid. In the last part, we saw that we added our authorization server endpoint as “iss” attribute in the token, which is why we have to pass the Authorization server endpoint as issuer param to the verify function

Finally, this function returns an object that has isValid flag and additional parsed information such as client id and scope.

index.mjs:

This file has our handler function which performs the following actions:

  • It first retrieves the token by calling getToken from helpers module
  • Then it calls verifyJWT function from jwtVerificationHelper module
  • Based on the response from verifyJWT function, it generates the policy by calling the generatePolicy function from helpers
  • Finally, it returns the policy

Here is the complete code for this function

import { verifyJWT } from './jwtVerificationHelper.mjs';
import { getToken, generatePolicy } from './helpers.mjs';

export const handler = async (event, context, callback) => {
try{
console.log(event);
let token = getToken(event.authorizationToken);
let verified = await verifyJWT(token);
console.log(verified);
if(verified.isValid){
let policy = generatePolicy(verified.clientId, 'Allow', verified.scope);
console.log(JSON.stringify(policy));
return policy;
}
return generatePolicy(verified.clientId, 'Deny', null);
}
catch(err){
console.error(err);
callback("Unauthorized");
}
};

package.json

As we used the jsonwebtoken npm package to verify the token, our package.json file will look like this:

{
"dependencies": {
"jsonwebtoken": "^9.0.0"
}
}

Find the complete code for this lambda here.

Now our Authorizer Lambda is ready, let’s attach it to our Resource server endpoint

Attach Authorizer Lambda with Resource Server:

First, we have to create an API Gateway Authorizer and then we can attach it to our resource endpoint. Please follow these steps to create and attach Authorizer to the resource endpoint:

  1. Within our Resource Server API, navigate to the Authorizers tab from the left menu and click on “Create New Authorizer”

2. Provide the Name for your Authorizer and leave the “Type” setting as Lambda.

3. In the “Lambda Function” field, type the name of your Authorizer Lambda and select it.

4. For the “Lambda Event Payload” setting, choose Token.

5. In the “Token Source” field, type “Authorization”.

6. Leave the other settings as default and click “Create”.

7. In the pop-up menu, click “Grant & Create” to allow API Gateway to invoke our Authorizer Lambda function

8. Now, navigate to the Resources tab and Click on the method (Get)

9. Click on “Method Request”

10. Edit the Authorization setting and select the lambda Authorizer we just created and attach it.

11. Now, Go ahead and deploy the API from the Actions menu

Now, our Resource server is ready, let’s test it

Test the Resource Server:

To start, again hit the resource endpoint without Access Token using the Postman request we created earlier. Now, our resource server will give “401 Unauthorized” response as shown in the image below:

Now, select OAuth 2.0 again in the Authorization tab and create a new Access Token. Hit the endpoint again and now our resource server will allow access to the resource.

Also, if we provide an invalid or an expired token then the resource server will give a “403 Forbidden” response as shown in the image below:

Now, everything looks good and we are able to access the protected resource.

Also, remember we had added permissions inside our Authorizer Lambda policy so that they are available in the Resource Lambda. To verify if the permissions are being passed to our resource lambda, we can console log the event object and check it in the CloudWatch logs:

Let’s go ahead and see this log in CloudWatch:

As evident from the image above, the permissions are in fact there in the event and we can use them to only perform the permitted actions.

Conclusion:

This was the last part of our series on implementing OAuth 2.0 client credentials flow using AWS technologies and securing API Gateway endpoints. In this part, we created our Resource server API Gateway and secured it by attaching an Authorizer Lambda function. As a result, we observed that the resource server endpoints were not accessible unless a valid access token is provided. Finally, we saw how we can pass permissions from the Authorizer Lambda to the Resource Lambda.

I hope you guys have enjoyed this series. Please share your thoughts in the comments!

References:

  1. Asymmetric JWT Signing using AWS KMS
  2. Use API Gateway Lambda authorizers
  3. Output from an Amazon API Gateway Lambda authorizer

--

--

Bilal Ashfaq

Software Engineer . Cloud Enthusiast . Passionate about building scalable software solutions and enhancing user experiences.