Securing Serverless APIs: A Deep Dive into Lambda Authorizers and JWT

Protect your serverless APIs with robust authentication and authorization. This tutorial provides a step-by-step guide to implementing custom Lambda authorizers with JSON Web Tokens (JWT) for fine-grained access control.

When you build a public-facing serverless API with Amazon API Gateway and AWS Lambda, one of the first questions you need to answer is: "How do I secure it?" While API Gateway offers several built-in authentication mechanisms, including IAM and Cognito, sometimes you need a more flexible, custom solution.

This is where Lambda authorizers shine. A Lambda authorizer is a function that you write to control access to your API. It's a powerful pattern that lets you integrate with any token-based authentication provider, such as Auth0, Okta, or your own custom identity service.

This guide provides a deep dive into building a Lambda authorizer that validates JSON Web Tokens (JWT).

The Authentication Flow

The process is straightforward:

  1. A client authenticates with an identity provider (e.g., Auth0) and receives a JWT.
  2. The client makes a request to your API Gateway endpoint, including the JWT in the Authorization header (e.g., Authorization: Bearer <your_jwt>).
  3. API Gateway invokes your Lambda authorizer function, passing it the token.
  4. Your Lambda authorizer validates the JWT's signature, expiration, and claims.
  5. If the token is valid, the authorizer returns an IAM policy that grants access to the requested endpoint. If it's invalid, it returns a policy that denies access.
  6. API Gateway uses this policy to either allow the request to proceed to your backend Lambda function or to return a 403 Forbidden error.

Step 1: Building the Lambda Authorizer Function

Let's create the core of our security mechanism: the authorizer function itself. This Python function will be responsible for parsing and validating the JWT.

We'll use the popular PyJWT library to handle the token validation. Make sure to include it in your function's deployment package.

import jwt
import os

# These should be configured securely, e.g., via environment variables or Secrets Manager
AUTH0_DOMAIN = os.environ.get('AUTH0_DOMAIN')
API_AUDIENCE = os.environ.get('API_AUDIENCE')
ALGORITHMS = ["RS256"]

def handler(event, context):
    """Validates a JWT and returns an IAM policy."""
    try:
        token = event['authorizationToken'].split(' ')[1] # Extract token from 'Bearer <token>'
        
        # Fetch the JSON Web Key Set (JWKS) from your Auth0 domain
        jwks_url = f'https://{AUTH0_DOMAIN}/.well-known/jwks.json'
        jwks_client = jwt.PyJWKClient(jwks_url)
        signing_key = jwks_client.get_signing_key_from_jwt(token).key

        # Decode and validate the token
        payload = jwt.decode(
            token,
            signing_key,
            algorithms=ALGORITHMS,
            audience=API_AUDIENCE,
            issuer=f'https://{AUTH0_DOMAIN}/'
        )

        # If validation is successful, return an 'Allow' policy
        policy = generate_policy(payload['sub'], 'Allow', event['methodArn'])
        return policy

    except Exception as e:
        print(f"Authentication error: {e}")
        # On any error, deny access
        return generate_policy('user', 'Deny', event['methodArn'])

def generate_policy(principal_id, effect, resource):
    """Helper function to create an IAM policy document."""
    return {
        'principalId': principal_id,
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                'Action': 'execute-api:Invoke',
                'Effect': effect,
                'Resource': resource
            }]
        }
    }

Key Points:

  • JWKS: The authorizer dynamically fetches the public signing keys from the identity provider's JWKS endpoint. This is a security best practice that allows for automatic key rotation.
  • Validation: jwt.decode is doing the heavy lifting. It verifies the token's signature, checks that it hasn't expired, and validates the audience and issuer claims to ensure the token was intended for your API.
  • Policy Generation: The generate_policy function constructs the IAM policy that API Gateway needs to see. A successful validation returns an Allow policy; any failure results in a Deny policy.

Step 2: Configuring API Gateway

Now, let's hook this function up to API Gateway.

  1. Create the Authorizer: In the API Gateway console, navigate to your API and select "Authorizers." Create a new Lambda authorizer.

    • Lambda Function: Choose the authorizer function you just created.
    • Lambda Event Payload: Select "Token."
    • Token Source: Enter Authorization for the header name.
    • Enable caching to improve performance and reduce costs.
  2. Attach the Authorizer to a Route: Go to the route you want to protect (e.g., GET /products). In the "Method Request" settings, select your newly created Lambda authorizer as the authorization type.

Step 3: Testing the Secured Endpoint

Now, if you try to access your endpoint without a valid JWT, you'll receive a 403 Forbidden response.

To access it successfully:

  1. Get a valid JWT from your identity provider.

  2. Make a request to the endpoint, including the token in the header:

    curl --request GET \
      --url https://<your-api-id>.execute-api.<region>.amazonaws.com/products \
      --header 'Authorization: Bearer <your_jwt>'
    

If the token is valid, API Gateway will invoke your backend Lambda function and return the response.

Conclusion

Lambda authorizers are a powerful and flexible tool for securing your serverless APIs. By combining them with a standard like JWT, you can easily integrate with a wide range of identity providers and implement robust, custom authentication logic.

This pattern provides a clean separation of concerns, keeping your business logic free of authentication code and centralizing your security in a single, reusable function.