Download as pdf or txt
Download as pdf or txt
You are on page 1of 34

Open in app

Search

Implementing OAuth 2.0 with AWS API


Gateway, Lambda, DynamoDB, and KMS —
Part 2
Bilal Ashfaq · Follow
11 min read · Aug 5, 2023

Listen Share More

This is the second article in the series to implement OAuth 2.0 Client Credentials
flow using AWS Serverless technologies. Please read the previous article here.

In this article, we will create an Authorization server that clients will use to get
Access Tokens. Before we start let’s have a look at the architecture diagram from the
previous article to revise our concepts.

Authorization Server — Architecture


As shown in the diagram, our Authorization Server has an API Gateway endpoint
that the client would call to get the access token. The lambda function attached to
the endpoint will authenticate the client i.e. verify that the client id and client secret
are valid and that the scopes requested by the client are permitted. For this, we have
a DynamoDB table that has the details of the registered clients. Finally, the lambda
function would create a token and sign it using KMS and return it to the client.

So, let’s start by creating the KMS key,

Create KMS Key:


Navigate to the Key Management Service (KMS) page on the AWS console and follow
these steps to create a KMS key that we will use to sign our tokens:

1. Click on the “Create Key” button

2. On the “Configure Key” page, select the following options: choose “Asymmetric”
as the key type, “Sign and Verify” for the Key Usage, and Key spec we can choose
RSA_2048. Once done, click next
3. On the “Add Labels” page, type the Alias for the key as shown below. Leave the
rest of the settings as they are and click next
4. Choose the Key administrators and check the box to allow key administrators to
delete the key. Click next
5. Choose key users and click next (we will later edit this setting and add our
Lambda role so that our lambda function can use this key)
6. Finally, review the settings and click on Finish.
Now that our KMS key is created, we can have a look at our clients DynamoDB table:

Clients Table (DynamoDB):


As stated earlier, we will use a DynamoDB table that will have the details of all the
registered clients(i.e. client credentials and scope). Moreover, we are going to
assume that clients have already acquired their credentials from the Authorization
server.

This table has the ClientId as the partition key, as shown in the image below,

Also, note that each record has three attributes, ClientId, ClientSecrect, and
AllowedScopes.

In our lambda function, we will use this table to authenticate the client and verify
that the requested scopes are permitted to the client.
Now, let’s go ahead and create our Lambda function:

Create 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, choose runtime Node.js 18.x, for Architecture choose x86_64,

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

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

helpers.mjs:

This module has the following three functions:

convertCredentialsStringToObject: which will convert the client credentials


string received in the request to an object for easy processing. For instance, it
will convert the following string:
“grant_type=client_credentials&scope=read%20write&client_id=1tvnd40m&clie
nt_secret=fcfff578–181e-11ee-be56–0242ac120002”

To the following object:

{
grant_type: ‘client_credentials’,
scope: ‘read write’,
client_id: ‘1tvnd40m’,
client_secret: ‘fcfff578–181e-11ee-be56–0242ac120002’
}

Here is the code for this function:

export const convertCredentialsStringToObject = (credentialsString) => {


let obj = {};
credentialsString.replace(/%20/g,' ').split('&').forEach(o => {
let values = o.split('=');
obj[values[0]] = values[1];
});
return obj;
};

customException: which just adds a code attribute to the default Error object of
JavaScript

export const customException = (code, message) => {


let error = new Error(message);
error.code = code;
return error;
};

createResponse: that takes statusCode and body as arguments and returns the
response object
export const createResponse = (statusCode, body) => {
return {
statusCode: statusCode,
body: JSON.stringify(body)
};
};

dbHelper.mjs:

This module has just one function “getClient” that will get the client details stored in
our DynamoDB table based on the client id provided by the client.

Here is the code for this file

import { DynamoDB } from "@aws-sdk/client-dynamodb"


import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
const ddb = new DynamoDB();

export const getClient = async ( clientId ) => {


console.log(clientId);
let params = {
Key: marshall({
ClientId: clientId
}),
TableName: "app-clients"
}

const { Item } = await ddb.getItem(params);


return unmarshall(Item);
}

In the function, we are using marshall and unmarshall functions. marshall is used
to convert a JavaScript object to a DynamoDB record while unmarshall is used to
convert a DynamoDB record to a JavaScript object.

validationHelper.mjs

This module has a function “validateCredentials” that takes the client id, client
secret, and scope as arguments and authenticates the client.

Here is the code for this file


import { getClient } from './dbHelper.mjs';
import { customException } from './helpers.mjs';

export const validateCredentials = async (clientId, clientSecret, scope) => {


let client;

try{
client = await getClient(clientId);
console.log("Client ", client);
if(!client) {
throw new Error("Client does not exist");
}
}
catch(error){
console.error(error);
throw customException(400, "Invalid Credentials");
}

if(client.ClientSecret !== clientSecret){


console.log("Secret is incorrect");
throw customException(400, "Invalid Credentials");
}
if(!scope.every(o => client.AllowedScopes.includes(o))){
throw customException(400, "Invalid Scopes");
}
}

In the function, we first call the getClient from dbHelper to verify that the client is
registered with our server. Then, we compare the secret provided by the client in
request with the one we got from db, and finally, we verify that all the scopes
requested by the client are permitted. If any of these conditions fail, we throw an
exception accordingly.

jwtHelper.mjs

This module has a method “getJsonWebToken” which creates and returns a JWT.

Here is the code for this file

import base64url from "base64url";


import { KMS } from "@aws-sdk/client-kms"
var kms = new KMS();
export const getJsonWebToken = async (clientId, scope) => {
let header = {
alg: "RS256",
typ: "JWT"
};

let payload = {
exp: Math.floor(Date.now() / 1000) + (60 * 60),
iat: Math.floor(Date.now() / 1000),
iss: '',
client_id: clientId,
scope: scope
};

let jwt_parts = {
header: base64url(JSON.stringify(header)),
payload: base64url(JSON.stringify(payload))
};

let params = {
KeyId: "KMS KEY ID",
Message: Buffer.from(jwt_parts.header + "." + jwt_parts.payload),
MessageType: "RAW",
SigningAlgorithm: "RSASSA_PKCS1_V1_5_SHA_256"
};

let signed_jwt = await kms.sign(params);


console.log(signed_jwt);

jwt_parts.signature = base64url(Buffer.from(signed_jwt.Signature));

return jwt_parts.header + "." + jwt_parts.payload + "." + jwt_parts.signatu


}

In the function, we first create a header object that has the following attributes:

“alg”: which is the algorithm used for signing the token i.e. RS256

“typ”: which is the type of token i.e. JWT

Then we create a payload object that has the following attributes:

“exp”: which is the time after which the token would expire. We have set the
expiration time to 1-hour

“iat”: which is the time at which the token was issued


“iss”: which represents the issuer of the token. Its value would be set to the API
Gateway URL that we will create in a minute

“client_id”: the id of the client to which this token is issued

“scope”: the scopes permitted by this token

After creating the header and payload, we convert both these objects to Base64URL
encoding. Then we send a request to KMS to sign our concatenated header and
payload and give us a signature. Note that in the parameters passed to the KMS sign
method, we have to provide the id of the same KMS key we created earlier.

Finally, we return our JWT which is a concatenation of the base64url encoding of


the header, payload, and signature.

index.mjs

This module has our handler function that first converts the Credentials string into
a JavaScript object by calling the method convertCredentialsStringToObject from
helpers module. Then, it converts the scopes into an array instead of space
separated string. After that, it calls the validateCredentials function from the
validationHelper module. Then, it calls the getJsonWebToken function from the
jwtHelper module and finally returns the access token to the client.

Here is the code for this file

import { getJsonWebToken } from './jwtHelper.mjs';


import { convertCredentialsStringToObject, createResponse } from './helpers.mjs
import { validateCredentials } from './ValidationHelper.mjs';

export const handler = async(event) => {


try{
console.log("Event ", event);
let requestBody = convertCredentialsStringToObject(event.body);
console.log("Request Body ", requestBody);

let scopeArray = requestBody.scope ? requestBody.scope.split(' ') : [];


await validateCredentials(requestBody.client_id, requestBody.client_sec
let jwt = await getJsonWebToken(requestBody.client_id, scopeArray);

return createResponse(200, { access_token: jwt });


}
catch(err){
console.error(err);
return createResponse(err.code ?? 500, err.message ?? "Internal Server
}
};

package.json

Also, note that we used the base64url npm package to encode our JWT parts, so our
package.json file will look like this:

{
"dependencies": {
"base64url": "^3.0.1"
}
}

Find the complete code for this lambda function here.

Now that our Lambda function is ready, we can go ahead and allow it access to the
KMS key

Allow Lambda Function role to Use KMS key:


Currently, our lambda function will not be able to use the KMS key, because it does
not have any permissions to use it. To allow our function to use the key, edit the key
and add a new key user as shown in the image below.

Search for the lambda role, select it, and add.


Now we are ready to create our API Gateway and connect it with our lambda
function.

Create API Gateway:


Follow these steps to set up our Authorization API Gateway:

1. Navigate to the API Gateway console and Select “Create API”

2. Choose Rest API and click on Build


3. In the Settings section, type the name for your API, leave the rest of the settings as
default, and click on Create API

4. Now, in the Actions dropdown select “Create Resource” and add the resource
name as “oauth”
5. Then, again create a nested resource inside oauth and name it “token”

6. Finally, from the Actions dropdown select “Create Method” inside the token
resource and select Post method
7. In the Integration type, we can see that the Lambda Function is selected by
default, leave it as it is. Check the “Use Lambda Proxy Integration” check box, which
just means that the API Gateway will pass the request as it is to the lambda
8. In the Lambda Function text box, type the name of your function and select it

9. When you will hit “Save”, Console will prompt you to give permission to API
Gateway to invoke the lambda. Select Ok to give permission.

10. Now from the Actions dropdown select “Deploy API” and in the pop up add a
new stage and click Deploy
After deploying the API, don’t forget to update the “iss” attribute value in the lambda
function with the URL of the API Gateway.

let payload = {
exp: Math.floor(Date.now() / 1000) + (60 * 60),
iat: Math.floor(Date.now() / 1000),
iss: 'https://<auth-server-api-id>.execute-api.us-east-1.amazonaws.com'
client_id: clientId,
scope: scope
}

Now our Authorization Server is ready, and we can go ahead and test it!

Get a Token from Authorization Server:


Please follow these steps to get an Access Token from the Authorization server we
just created:

Open up Postman or any other API testing tool of your choice and create a new
request.

Switch to the Authorization tab and in the type dropdown select OAuth 2.0.
In the Grant Type select “Client Credentials” and in the “Access Token URL” field,
add the URL of our authorization server endpoint. Also, provide the values for Client
ID, Client Secret, and Scope. Finally, in the Client Authentication dropdown select
“Send client credentials in body”.
Now, click on the “Get New Access Token” Button and you should see a popup with
the message “Authentication Complete”. Click Proceed to view the token.
Now try to provide a scope that is not permitted to the client and get the token again.
You should see an error like this:
Now, Generate an Access Token with permitted Scopes and open jwt.io. Copy the
Token and paste it into the Encoded section of the debugger. In the Decoded section,
we will be able to see all the details we added in the token, i.e. headers, and payload.

But note that the debugger currently says that the signature is invalid because we
have not provided the public key to validate the signature.
Now, Open the KMS key and switch to the Public Key tab. Click on the “Copy” button
to copy the key.

Paste the key into the jwt.io debugger to verify the signature.
Now, we can see that the signature is verified.

Conclusion:
In this article, we set up our authorization server API Gateway. To achieve this, we
first created a KMS key that we later used to sign the token. Then, we saw the
structure of our clients DynamoDB table. Further, we created a lambda function and
saw the complete code of this function. After that, we created an API Gateway
resource and connected it with our lambda function. Finally, we tested our
Authorization server through Postman.

In the next part of this series, we will create our Resource Server and will see how to
get the resource by providing an access token.

References:
Asymmetric JWT Signing using AWS KMS

Oauth2 Jwt Aws Kms Authorization Api Gateway

Follow

Written by Bilal Ashfaq


10 Followers

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

More from Bilal Ashfaq

Bilal Ashfaq
Implementing OAuth 2.0 with AWS API Gateway, Lambda, DynamoDB,
and KMS — Part 3
This is the third article in the series to implement OAuth 2.0 Client Credentials flow using AWS
Serverless technologies. In the previous…

8 min read · Aug 5, 2023

Bilal Ashfaq

Implementing OAuth 2.0 with AWS API Gateway, Lambda, DynamoDB,


and KMS — Part 1
In this series, we will see how we can secure our API Gateway endpoints by implementing
OAuth 2.0 client credentials flow using various AWS…

8 min read · Aug 5, 2023

See all from Bilal Ashfaq


Recommended from Medium

Cumhur Akkaya

What are an API and Amazon API Gateway? Creating and Using a REST
API with Amazon API Gateway.
We will learn details knowledge about the API and the Amazon API Gateway. Then, we will
create our first API with Amazon API Gateway in the…

16 min read · Oct 8, 2023

18
Leo Cherian

Amazon API gateway with Cognito user pool


Amazon API Gateway is an AWS service for creating, publishing, maintaining, monitoring, and
securing REST, HTTP, and WebSocket APIs at any…

5 min read · Aug 2, 2023

68

Lists

Staff Picks
563 stories · 662 saves

Stories to Help You Level-Up at Work


19 stories · 430 saves

Self-Improvement 101
20 stories · 1243 saves

Productivity 101
20 stories · 1143 saves
Thuong To

Event-Driven Architectures on AWS


This week’s customer currently uses a synchronous web application to host the orders service,
which is causing various issues — for…

6 min read · Dec 6, 2023

Talha Şahin
High-Level System Architecture of Booking.com
Hello everyone! In this article, we will take an in-depth look at the possible high-level
architecture of Booking.com, one of the world’s…

8 min read · Jan 10

1.4K 9

Nidhey Indurkar

How did PayPal handle a billion daily transactions with eight virtual
machines?
I recently came across a reddit post that caught my attention: ‘How PayPal Scaled to Billions of
Transactions Daily Using Just 8VMs’…

7 min read · Jan 1

3.2K 34
Madhusudhanan in Level Up Coding

Learn how to secure Spring Boot application using Amazon Cognito


In this blog, we’ll learn how to secure a Spring boot web application using Amazon Cognito. If
you’re already familiar with the basics of…

5 min read · Dec 18, 2023

116

See more recommendations

You might also like