DEV Community

Jap Purohit
Jap Purohit

Posted on • Updated on

AWS API Gateway and Custom Authorizer

AWS API Gateway is a fully managed service that enables developers to create, publish, and manage APIs (Application Programming Interfaces) at scale. Without worrying about server infrastructure, capacity planning, or network security, APIs can be created as well as managed easily. Because of this, it is an essential part of serverless architecture and microservices-based applications.

One of the most important features of the AWS API Gateway is the ability to add custom authorizers to secure your APIs. Custom authorizers are functions that run on AWS Lambda, which can validate incoming requests and determine whether or not they should be authorized. This enables you to extend the functionality of API's authentication and authorization logic beyond what is offered by the standard AWS security features. The following image describes about the basic architecture of the AWS API Gateway along with Custom Authorizer:

Basic Architecture of API Gateway and its working (Credit: Alex DeBrie)

Credit : Alex DeBrie

In this article, we will cover working of custom authorizer, steps to create it and integrate with AWS API gateway. We have also highlighted some of the benefits and best practices of using custom authorizers.

How Custom Authorizers Work

A custom authorizer is a Lambda function that is triggered before the request is passed to the backend. It accesses the incoming request, validate the request, and generate a policy that either allows or denies access to the backend.

To use a custom authorizer, you first need to create a Lambda function that implements your authorization logic. The Lambda function must return a policy document that specifies the permissions for the incoming request. The policy document is a JSON document that includes information about the principal (the user or system making the request), the action (the operation being performed), and the resource (the API endpoint being accessed).

Once the custom authorizer is created, you can configure API Gateway to use it for your APIs. When you create an API, you can specify the custom authorizer to use for each endpoint or group of endpoints.

You can also pass information from the incoming request to the custom authorizer, such as headers, query parameters, and path variables. This information can be used to generate the policy document and determine whether the request is authorized.

Creating a Custom Authorizer

To create a custom authorizer, you first need to create a Lambda function. This function will be responsible for checking incoming requests and determining whether they should be authorized.

Here are the steps to create a custom authorizer in AWS API Gateway:

  1. Create a new Lambda function: To create a new Lambda function, go to the AWS Lambda console, and click on the “Create Function” button. Select “Author from scratch” and give the function a name. Choose the runtime environment that you want to use, such as Node.js or Python.
  2. Define the custom authorizer logic: The next step is to define the custom authorizer logic. The custom authorizer logic should include code to validate incoming requests and determine if they should be authorized. The custom authorizer should also return an IAM policy that determines the actions that can be performed by the authenticated user.
  3. Connect the Lambda function to API Gateway: Once the lambda function is created, it has to be connected to the API Gateway. To do this, go to the API Gateway console, and create a new API. In the “Authorization” section, select “Custom Authorizer” and select the Lambda function you just created.
  4. Configure the API Gateway resource: After connecting the Lambda function to API Gateway, the next step is to configure the API Gateway resource. This involves defining the methods that will be allowed on the API, such as GET, POST, PUT, and DELETE. You can also define the URL parameters, query string parameters, and request bodies that will be accepted by the API.
  5. Deploy the API: The final step is to deploy the API. To deploy the API, go to the API Gateway console, and click on the “Deploy API” button. You can select the desired stage and the deployment options, and then click on the “Deploy” button.
  6. Once the API is deployed, you can start using the custom authorizer to secure your APIs. The custom authorizer will be triggered every time a request is made to your API, and it will determine if the request is authorized. If the request is not authorized, the custom authorizer will return a 401 Unauthorized response.

Benefits of using Custom Authorizers

There are several benefits to using custom authorizers in API Gateway:

  1. Flexibility: With custom authorizers, you have complete control over the authorization logic, allowing you to implement any custom authentication or authorization logic you need.
  2. Integration with Third-Party Services: Custom authorizers allow you to integrate your API with third-party authentication and authorization services, such as Auth0 or Okta.
  3. Reusability: Custom authorizers can be reused across multiple APIs, allowing you to centralize your authorization logic and reduce duplication.
  4. Performance: Custom authorizers run as Lambda functions, which can scale automatically to handle large amounts of traffic. This eliminates the need for you to manage the infrastructure for your authorization logic.

Best Practices for using custom authorizers

Here are some best practices for securing your APIs with custom authorizers:

  1. Use Encryption: When storing sensitive information, such as credentials, it's important to encrypt the data. You can use the AWS Key Management Service (KMS) to encrypt your data at rest and in transit.
  2. Validate Input: It's important to validate all incoming requests to ensure that they contain the necessary information and are well-formed. You can use the JSON schema validation feature in API Gateway to validate incoming requests.
  3. Implement Least Privilege: When generating the policy document, it's important to grant the minimum permissions necessary to perform the requested operation. This is known as the principle of least privilege.
  4. Rotate Credentials Regularly: It's important to rotate your credentials regularly to reduce the risk of unauthorized access. You can automate this process using AWS Secrets Manager.

Useful Library which increases Code Readability

  1. distinction-dev/lambda-authorizer-utils: This node package is one of the most easy to use package. With just few lines of code, one can create custom authorizer with custom response. This package could be beneficial when using validation token for authorization. Also, it uses caching concept which means that token is cached in the API Gateway for maximum one hour. With caching in place, it reduces the need to call lambda function for each API call, eventually making the responses faster. Additionally, API Gateway returns policies from caching till the time it is valid. Hence, using this package not only helps with increasing code readability, but also helps in reducing time and space complexity.
const jwtDecode = require('jwt-decode')
const { AuthorizerResponse, DENY_ALL_RESPONSE, Verifier } = require('@distinction-dev/lambda-authorizer-utils')
const models = require('../models')

/**
 * Gets the user from Database,
 * ToDo- Cache the response once and only use it
 * @param {string} emailId
 * @returns {Promise<import('./types').UserFromDb>}
 */
const findUserDetails = async (emailId) => {
  try {
    const options = {
      where: {
        email: emailId
      },
      raw: true,
    }
    const user = await models.User.findOne(options)
    return user
  } catch (err) {
    console.error(err)
    throw err
  }
}

/**
 * Handler for the authorizer
 * @param {import("aws-lambda").APIGatewayRequestAuthorizerEvent} event
 * @param {import("aws-lambda").Context} context
 * @returns {Promise<import('@distinction-dev/lambda-authorizer-utils').AwsPolicy | typeof DENY_ALL_RESPONSE>}
 */
exports.handler = async function (event, context) {
  console.log('Event : ',event)
  const { methodArn } = event
  console.log('Method ARN', methodArn)
  const arnSplit = methodArn.split('/')
  console.log('ARN Split', arnSplit)
  const apiGwStage = arnSplit[1]
  try {
    const authHeader = event.headers?.Authorization || event.headers?.authorization //Get authorization token from the request headers
    const arnParts = AuthorizerResponse.parseApiGatewayArn(event.methodArn) // Extract MethodArn that user has requested for 
    const response = new AuthorizerResponse(
      'apigateway.amazonaws.com',
      arnParts.region,
      arnParts.awsAccountId,
      arnParts.apiId,
      arnParts.stage
    ) //Generate authorize response that needs to be sent after validation
    if (authHeader) {
      const token = Verifier.getCleanedJwt(authHeader) // Get clean token i.e. remove Bearer from the token if present
      console.log(token)
      if (token) {
        if (token === 'dev-token') {
          /** This code used for passing static token for local development */
          if (apiGwStage === 'local') {
            response.allowAllRoutes()
          } else{
            response.context = {
              'stringKey': 'Invalid API GW Stage'
            }
            response.denyAllRoutes()
          }
        } else {
          /**
           * @type {import('./types').UserClaims}
           */
          console.log(jwtDecode(token))
          const { sub: userEmail } = jwtDecode(token)
          const user = await findUserDetails(userEmail)
          if (user) {
            response.context = {...user, stringKey: 'Authorization Successful'}
            response.allowAllRoutes()
          } else {
            console.error('Authorization Failed: user not found in database.')
            response.context = {
              'stringKey': 'You are not authorized to access'
            }
            response.denyAllRoutes()
          }
        }
      }else{
        response.context = {
          'stringKey': 'Authorization Valid Token Missing'
        }
        response.denyAllRoutes()
      }
    }
    else{
      response.context = {
        'stringKey': 'Authorization Token Missing'
      }
      response.denyAllRoutes()
    }
    console.log(response)
    return response.getPolicy()
  } catch (error) {
    console.log('Authorization Failed: ', error.message || error)
    return DENY_ALL_RESPONSE
  }
}
Enter fullscreen mode Exit fullscreen mode

Changes in serverless.yml for custom response:

resources:
  Resources:
    GatewayResponseUnauthorized:
      Type: AWS::ApiGateway::GatewayResponse
      Properties:
        ResponseType: UNAUTHORIZED
        RestApiId:
          Ref: "ApiGatewayRestApi"
        ResponseParameters:
          gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
          gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
        ResponseTemplates:
          "application/json": '{"message": $context.authorizer.stringKey}'

    GatewayResponseAccessDenied:
      Type: AWS::ApiGateway::GatewayResponse
      Properties:
        ResponseType: ACCESS_DENIED
        RestApiId:
          Ref: "ApiGatewayRestApi"
        ResponseParameters:
          gatewayresponse.header.Access-Control-Allow-Origin: "'*'"
          gatewayresponse.header.Access-Control-Allow-Headers: "'*'"
        ResponseTemplates:
          "application/json": '{"message": $context.authorizer.stringKey}'
Enter fullscreen mode Exit fullscreen mode

I hope you found this article informative and useful. Do let me know your thoughts and do not forget to like, comment and share this article to your coder community.

Top comments (0)