Serverless architecture has gained immense popularity due to its scalability, cost-effectiveness, and ease of development. With AWS lambda and API Gateway, we can build such APIs that are scalable and secure. There are two ways we can authorize our APIs: Cognito Authorizer and Lambda Authorizer. In this article, we will cover how we can secure APIs using the Lambda Authorizer.
We’ll explore how to implement a Lambda Authorizer using JSON Web Tokens (JWT) for API Gateway authentication. We’ll create two Lambda functions – test-function
that exposes an endpoint through API Gateway that utilizes the Lambda Authorizer, and my-authorizer
responsible for authorizing requests based on bearer tokens. In this example, we will use NodeJS as the server runtime.
Creating Lambda Authorizer Function:
The function my-authorizer
is responsible for validating the Authorization token from the request. If the validation is successful the request goes to the next Lambda function. Otherwise, it returns 401 Unauthorized.
Let’s create the lambda function ‘my-authorizer’
import jwt from 'jsonwebtoken';
class TokenManager {
#JWT_SECRET; // private attribute
constructor() {
this.#JWT_SECRET = 'JUSTASECREC-35';
}
createToken(data) {
if (!data) return false;
try {
return jwt.sign({
exp: Math.floor(Date.now() / 1000) + (60 * 60),
data: DUMMY_USER
}, this.#JWT_SECRET);
} catch (err) {
return false;
}
}
validateToken(token) {
if (!token) return false;
try {
return jwt.verify(token, this.#JWT_SECRET);
} catch (err) {
return false;
}
}
}
import jwt from 'jsonwebtoken';
:- Variable ‘jwt‘ will include functionalities from the ‘jsonwebtoken‘ library. The ‘jwt‘ will be used for creating and verifying JSON Web Tokens (JWT).
class TokenManager { ... }
:- Defines a class named
TokenManager
responsible for managing JWT tokens.
- Defines a class named
#JWT_SECRET;
:- Declares a private attribute
#
JWT_SECRET
to store the secret key for JWT. Please note: Private attributes are denoted by the#
prefix in JavaScript.
- Declares a private attribute
constructor() { ... }
:- Initializes the
#
JWT_SECRET
private attribute with a secret string when an instance ofTokenManager
is created.
- Initializes the
createToken(data) { ... }
:- This method will generate a new JWT token. We have commented. However, you can uncomment to get a token.
- Checks if
data
is provided; if not, it returnsfalse
. - Uses
jsonwebtoken
to sign the token with an expiration time and some data (here, theDUMMY_USER
object).
validateToken(token) { ... }
:- The function will validate a JWT token passed through the request header.
- Checks if
token
is provided; if not, it returnsfalse
. - Uses
jsonwebtoken
to verify the token against the stored secret key.
const DUMMY_USER = {
id: 270120241803,
email: 'johndoe@gmail.com',
name: 'John Doe'
};
DUMMY_USER
: This is a constant variable that contains a sample user object with an ID, email, and name. This user data will be included in the JWT token.
let getAuthToken = (event) => {
try {
let token = event.headers.Authorization.split('Bearer')[1].trim();
return token;
} catch (err) {
return null;
}
};
getAuthToken(event)
: getAuthToken
function takes the ‘event‘ object and returns the JWT token from the Authorization header from the event object.
export const handler = async function (event, context, callback) {
let tm = new TokenManager();
// let token = tm.createToken(DUMMY_USER); // to generate a new token
// console.log(token);
let tokenResponse = tm.validateToken(getAuthToken(event)); // validating the token
if (!tokenResponse)
callback(null, generatePolicy('user', 'Deny', event.methodArn)); // unauthorized
callback(null, generatePolicy('user', 'Allow', event.methodArn, DUMMY_USER)); // authorized
};
export const handler = async function (event, context, callback)
- Exports the main Lambda function named
handler
. - This function will be the entry point of the Lambda function.
- Exports the main Lambda function named
let tm = new TokenManager();
:- ‘tm’ is an instance of
TokenManager
class to manage JWT tokens.
- ‘tm’ is an instance of
let tokenResponse = tm.validateToken(getAuthToken(event));
:- Calls the
validateToken
method ofTokenManager
to validate the JWT token extracted from the request.
- Calls the
if (!tokenResponse) callback(null, generatePolicy('user', 'Deny', event.methodArn));
:- If the token is not valid, deny the request by calling the
generatePolicy
function with a “Deny” policy.
- If the token is not valid, deny the request by calling the
callback(null, generatePolicy('user', 'Allow', event.methodArn, DUMMY_USER));
:- If the token is valid, allow the request by calling the
generatePolicy
function with an “Allow” policy and include theDUMMY_USER
object as context information.
- If the token is valid, allow the request by calling the
let getAuthToken = (event) => {
try {
let token = event.headers.Authorization.split('Bearer')[1].trim();
return token;
} catch (err) {
return null;
}
};
getAuthToken(event)
: A helper function that extracts the JWT token from the Authorization header in the event object.
let generatePolicy = (principalId, effect, resource, authData = null) => {
var authResponse = {};
authResponse.principalId = principalId;
if (effect && resource) {
var policyDocument = {};
policyDocument.Version = '2012-10-17';
policyDocument.Statement = [];
var statementOne = {};
statementOne.Action = 'execute-api:Invoke';
statementOne.Effect = effect;
statementOne.Resource = resource;
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument;
}
authResponse.context = authData;
return authResponse;
};
generatePolicy(principalId, effect, resource, authData = null)
: A helper function that creates an authorization policy response.principalId
: The principal ID for the policy.effect
: The effect of the policy (Allow or Deny).resource
: The resource to which the policy applies.authData
: This is an optional context information to be included in the response. The Lambda authorizer will pass the Auth Data with the event object to the next lambda function, like a middleware, as we set ‘authResponse.context‘ with the ‘authData‘ in thegeneratePolicy
function.
This function constructs an authorization response with a policy document and context information, which is returned to the Lambda callback.
Creating The Lambda Function for our API:
It’s a pretty simple function that will return the Auth data along with the request. The Auth data will come from the Lambda Authorizer on successful authorization.
export const handler = async (event, context) => {
const response = {
statusCode: 200,
body: JSON.stringify(event.requestContext.authorizer), // passed auth data from lambda authorizer
};
return response;
};
We have completed two lambda functions. Now, it’s time to create our secured Rest API using API Gateway.
Configure API Gateway
Now, configure the API Gateway to expose the /userdetail
endpoint and secure it using the Lambda Authorizer.
- Create API Gateway:
- Go to the AWS Management Console and navigate to API Gateway.
- Click on “Create API” and choose “REST API.“
- Set a name for your API (e.g., “MySecureAPI”).
- Click “Create API.“
- Create Resource and Method:
- In the API Gateway console, select the API you just created.
- Under “Develop,” choose “REST API.”
- Create a new resource (e.g.,
/userdetail
) and add a GET method.
- Integration with Lambda Function:
- Configure the GET method to integrate with your
Lambda function.test-function
- Set the Lambda function as the integration for the method.
- Configure the GET method to integrate with your
- Create Lambda Authorizer:
- In the API Gateway console, select “Authorizers” in the left navigation pane.
- Click “Create Authorizer.”
- Choose the type “Lambda Function” and select the
my-authorizer
Lambda function. - Set a name (e.g., “my-lambda-authorizer“) and click “Create.”
- You can also test the authorizer from the console.
- Secure the
/userdetail
Resource:- Go back to the
/
userdetail
resource in your API. - Under the “Method” settings, choose the Lambda Authorizer you created in the “Authorization” section.
- Go back to the
- Deploy the API:
- Deploy your API by selecting “Deployments” in the left navigation pane.
- Create a new deployment stage (e.g., “prod”).
- Note the URL of your deployed API.
Let’s test the secured API
- Obtain a JWT Token:
- Use the createToken function to generate a token that is under the
TokenManager
class. Update theAuthorization
header with the token when making requests.
- Use the createToken function to generate a token that is under the
- Test Authorization:
- Make a GET request to the
/userdetail
endpoint using a tool like curl, Postman, or any HTTP client. - Include the JWT token in the Authorization header.
- Verify that the request is authorized by checking the response.
- Here’s the sample cURL
- Make a GET request to the
curl --location 'https://dp021k6raj.execute-api.us-west-2.amazonaws.com/test/userdetail' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MDYzNjQ1NDksImRhdGEiOnsiaWQiOjI3MDEyMDI0MTgwMywiZW1haWwiOiJqb2huZG9lQGdtYWlsLmNvbSIsIm5hbWUiOiJKb2huIERvZSJ9LCJpYXQiOjE3MDYzNjA5NDl9.N96UwwCc1Dy-HQ5MsoaEK4RF4ElTBCXj7gT_BQxhHzw'
Now, your /
userdetail
endpoint is secured using the Lambda Authorizer, ensuring that only authenticated and authorized requests are allowed.
Summary
In this article, we have implemented a secured API using Lambda Authorizer. This approach ensures that the API endpoints can only be accessed by authorized users. Stay tuned for our future articles, where we will explore the integration of Amazon Cognito as an alternative and powerful authorizer for enhanced user management and authentication in serverless applications.