Securing endpoints using API Gateway and Lambda Authorizer

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;
        }
    }
}
  1. 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).
  2. class TokenManager { ... }:
    • Defines a class named TokenManager responsible for managing JWT tokens.
  3. #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.
  4. constructor() { ... }:
    • Initializes the #JWT_SECRET private attribute with a secret string when an instance of TokenManager is created.
  5. 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 returns false.
    • Uses jsonwebtoken to sign the token with an expiration time and some data (here, the DUMMY_USER object).
  6. validateToken(token) { ... }:
    • The function will validate a JWT token passed through the request header.
    • Checks if token is provided; if not, it returns false.
    • 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
};
  1. 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.
  2. let tm = new TokenManager();:
    • ‘tm’ is an instance of TokenManager class to manage JWT tokens.
  3. let tokenResponse = tm.validateToken(getAuthToken(event));:
    • Calls the validateToken method of TokenManager to validate the JWT token extracted from the request.
  4. 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.
  5. 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 the DUMMY_USER object as context information.
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 the generatePolicy 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.

Lambda Function List

Configure API Gateway

Now, configure the API Gateway to expose the /userdetail endpoint and secure it using the Lambda Authorizer.

  1. 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.
  2. 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.
  3. Integration with Lambda Function:
    • Configure the GET method to integrate with your test-function Lambda function.
    • Set the Lambda function as the integration for the method.
  4. 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.
Test Authorizer
  1. 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.
Lambda Authorizer using Method Request
  1. 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

  1. Obtain a JWT Token:
    • Use the createToken function to generate a token that is under the TokenManager class. Update the Authorization header with the token when making requests.
  2. 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
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.

Lambda Authorizer GitHub Repository

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top