🔐 Secure API with Auth0: Custom Lambda Authorizers & JWT Authentication

Auth0 authentication with the Serverless Framework

Introduction

Auth0 is a cloud-based identity and access management service that provides authentication and authorization solutions for applications. It allows developers to integrate user authentication into their applications with minimal effort. Auth0 supports a variety of authentication methods.

This project demonstrates two methods of integrating Auth0 authentication with the Serverless Framework for securing AWS Lambda APIs:

  1. JWT-based authentication using API Gateway
  2. Lambda-based custom authorizer

Both methods provide robust security for your serverless applications, but they have different use cases and trade-offs.

Prerequisites

  1. Auth0 Account: Create an account at Auth0.
  2. Node.js and NPM: Installed on your system.
  3. Serverless Framework: Installed globally using:
    npm install -g serverless
    

JWT-based Authentication

This method uses API Gateway’s built-in JWT authorizer to validate tokens issued by Auth0.

Setup

  1. Create a new Auth0 application and API as described in the Auth0 documentation.
  2. Note your Auth0 domain and API audience.
  3. Update your serverless.yml:
provider:
  name: aws
  runtime: python3.12
  environment:
    AUTH0_DOMAIN: ${env:AUTH0_DOMAIN}
    AUTH0_AUDIENCE: ${env:AUTH0_AUDIENCE}
  httpApi:
    authorizers:
      auth0Authorizer:
        identitySource: $request.header.Authorization
        issuerUrl: https://${self:provider.environment.AUTH0_DOMAIN}/
        audience:
          - ${self:provider.environment.AUTH0_AUDIENCE}
        type: jwt
        id: auth0Authorizer

functions:
  hello:
    handler: handlers/hello.handler
    events:
      - httpApi:
          path: /hellojwtauth
          method: get
          authorizer: auth0Authorizer

Implementation

Create a Lambda function (handlers/hello.py) to handle the authenticated request:

def handler(event, context):
    claims = event.get("requestContext", {}).get("authorizer", {}).get("jwt", {}).get("claims", {})
    user_id = claims.get("sub", "Unknown User")

    return {
        "statusCode": 200,
        "body": f"Hello, {user_id}!"
    }

This setup configures an AWS API Gateway with JWT-based authentication using Auth0. The serverless.yml file sets up the JWT authorizer to validate tokens issued by Auth0 and protect the /hellojwtauth endpoint. The Lambda function extracts the user’s identity from the JWT claims and returns a personalized response.

Lambda-based Custom Authorizer

This method uses a custom Lambda function to validate tokens and provide fine-grained access control.

Setup

  1. Create a new Auth0 application and API as described in the Auth0 documentation.
  2. Note your Auth0 domain and API audience.
  3. Update your serverless.yml:
provider:
  name: aws
  runtime: python3.12
  environment:
    AUTH0_DOMAIN: ${env:AUTH0_DOMAIN}
    AUTH0_AUDIENCE: ${env:AUTH0_AUDIENCE}
  httpApi:
    authorizers:
      customAuthorizer:
        type: request
        functionName: lamauthorizer

functions:
  hellolambdaAuth:
    handler: handlers/helloauth.handler
    events:
      - httpApi:
          path: /hellolambdaauth
          method: get
          authorizer:
            name: customAuthorizer
  lamauthorizer:
    handler: handlers/authorizer.handler

Implementation

  1. Create a custom authorizer function (handlers/authorizer.py):
import jwt 
from jwt import ExpiredSignatureError, InvalidTokenError
import os
import time

def handler(event, context):
    authorization_header = event.get('headers', {}).get('authorization')
    if not authorization_header:
        return generate_policy('user', 'Deny', 'Unauthorized')

    token = authorization_header.split(' ')[-1]

    try:
        decoded_token = jwt.decode(token, options={"verify_signature": False})

        if decoded_token.get('iss') != f"https://{os.environ.get('AUTH0_DOMAIN')}/":
            return generate_policy('user', 'Deny', 'InvalidIssuer')

        if decoded_token.get('exp') < time.time():
            return generate_policy('user', 'Deny', 'TokenExpired')

        return generate_policy('user', 'Allow', 'AccessGranted')

    except (ExpiredSignatureError, InvalidTokenError):
        return generate_policy('user', 'Deny', 'InvalidToken')

def generate_policy(principal_id, effect, message):
    return {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': effect,
                'Resource': '*',
            }]
        },
        'context': {
            'message': message
        }
    }
  1. Create a Lambda function (handlers/helloauth.py) to handle the authenticated request:
def handler(event, context):
    claims = event.get("requestContext", {}).get("authorizer", {}).get("domainName", {})
    user_id = claims.get("sub", "Unknown User")

    return {
        "statusCode": 200,
        "body": "Hello from lambda auth!"
    }

This setup uses a custom Lambda function as an authorizer to validate Auth0 JWT tokens and enforce access control in AWS API Gateway. The serverless.yml configures the lamauthorizer Lambda function to check the token’s validity, issuer, and expiration. The Lambda function extracts user identity from the token and grants or denies access based on these checks, while the helloauth function responds to authenticated requests.

Comparison and Best Practices

JWT-based Authentication (API Gateway)

Pros:

  • Simpler setup and management
  • Reduced latency (no additional Lambda invocation)
  • Automatic token validation by API Gateway

Cons:

  • Limited customization options
  • Less flexibility for complex authorization logic

Best for:

  • Simple authentication requirements
  • High-performance APIs
  • When you need only basic claims validation

Lambda-based Custom Authorizer

Pros:

  • Highly customizable authorization logic
  • Can integrate with other services or databases
  • Supports complex access control policies

Cons:

  • Increased latency due to additional Lambda invocation
  • More complex setup and management
  • Potential for higher costs due to additional Lambda executions

Best for:

  • Complex authorization requirements
  • Integration with external systems for authorization
  • When you need fine-grained access control

Conclusion

Both JWT-based authentication and Lambda-based custom authorizers provide secure ways to protect your serverless APIs with Auth0. Choose the method that best fits your project’s requirements, considering factors such as complexity, performance, and customization needs.

For simple authentication scenarios, the JWT-based method using API Gateway is often sufficient and easier to implement. For more complex authorization logic or when you need to integrate with other systems, the Lambda-based custom authorizer provides greater flexibility and control.

Remember to always follow security best practices, keep your Auth0 configuration up to date, and regularly review and test your authentication and authorization mechanisms.

Get Started

To get started and try out the code:

1. Clone the Repository

Open your terminal and execute the following command:

git clone git@ssh.dev.azure.com:v3/7EDGEx/Backend/Backend

References

6 Likes