Implementing OAuth 2.0 With AWS API Gateway, Lambda, DynamoDB, and KMS - Part 2 - by Bilal Ashfaq - Medium
Implementing OAuth 2.0 With AWS API Gateway, Lambda, DynamoDB, and KMS - Part 2 - by Bilal Ashfaq - Medium
Search
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.
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:
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:
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:
{
grant_type: ‘client_credentials’,
scope: ‘read write’,
client_id: ‘1tvnd40m’,
client_secret: ‘fcfff578–181e-11ee-be56–0242ac120002’
}
customException: which just adds a code attribute to the default Error object of
JavaScript
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.
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.
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");
}
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.
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"
};
jwt_parts.signature = base64url(Buffer.from(signed_jwt.Signature));
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
“exp”: which is the time after which the token would expire. We have set the
expiration time to 1-hour
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.
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.
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"
}
}
Now that our Lambda function is ready, we can go ahead and allow it access to the
KMS key
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!
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
Follow
Software Engineer . Cloud Enthusiast . Passionate about building scalable software solutions and enhancing
user experiences.
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…
Bilal Ashfaq
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…
18
Leo Cherian
68
Lists
Staff Picks
563 stories · 662 saves
Self-Improvement 101
20 stories · 1243 saves
Productivity 101
20 stories · 1143 saves
Thuong To
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…
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’…
3.2K 34
Madhusudhanan in Level Up Coding
116