Skip to content

Instantly share code, notes, and snippets.

@jordotech
Last active July 2, 2025 15:51
Show Gist options
  • Save jordotech/a5510d3abcfcb520bb97d083a1d6859d to your computer and use it in GitHub Desktop.
Save jordotech/a5510d3abcfcb520bb97d083a1d6859d to your computer and use it in GitHub Desktop.
auth0_authorizer
IMAGE_TAG=
AUTH0_M2M_CLIENT_SECRET=
AUTH0_M2M_CLIENT_ID=
AUTH0_DOMAIN=changeme.us.auth0.com
AUTH0_JWKS_URI=https://changeme/.well-known/jwks.json

Auth0 JWT Authorizer for AWS API Gateway

A custom Lambda authorizer that validates JWT tokens issued by Auth0 for securing AWS API Gateway endpoints.

The terraform code that deploys this lambda code lives here: https://github.com/Faction-V/gofigure_terraform/tree/main/lambdas/auth0-authorizer

Overview

This Lambda function acts as a custom authorizer for AWS API Gateway, validating JWT tokens issued by Auth0. It verifies the token's signature, expiration, audience, and issuer claims before allowing access to protected API endpoints.

Features

  • JWT token validation using Auth0 JWKS (JSON Web Key Set)
  • Token caching for improved performance
  • Detailed error handling and logging
  • Sentry integration for error tracking
  • Containerized deployment using Docker
  • Easy local development and testing

Architecture

The authorizer follows this flow:

  1. API Gateway receives a request with an Authorization header
  2. The Lambda authorizer extracts the JWT token from the header
  3. It fetches the JWKS from Auth0 (with caching)
  4. It verifies the token signature, expiration, audience, and issuer
  5. If valid, it generates an IAM policy allowing access to the requested resource
  6. It passes user claims (scope, userId, email) to the API as context

Prerequisites

  • Python 3.12+
  • Docker
  • AWS CLI configured
  • Auth0 account with an API configured
  • Just command runner (optional, but recommended)

Environment Variables

The following environment variables must be set:

Variable Description
AUTH0_DOMAIN Your Auth0 domain (e.g., your-tenant.auth0.com)
AUTH0_AUDIENCE The API identifier in Auth0
AUTH0_JWKS_URI The JWKS URI (typically https://your-tenant.auth0.com/.well-known/jwks.json)
AWS_REGION AWS region for deployment (default: us-east-1)
ENVIRONMENT Environment name for Sentry (default: dev)
IMAGE_TAG Docker image tag for deployment

Local Development

Setup

  1. Clone the repository
  2. Install dependencies:
    poetry install

Running Locally with Docker

Build and run the Docker container:

just build-and-run

Or step by step:

just build
just run

Test the authorizer:

just ping

Access the container shell:

just ssh

Stop the container:

just stop

Deployment

Building and Pushing to ECR

  1. Create an ECR repository (if it doesn't exist):

    just create-ecr-repository
  2. Build the Docker image for ECR:

    just build-for-ecr
  3. Push the image to ECR:

    just push-to-ecr

Or do both in one step:

just build-and-push

Deploying to AWS Lambda

After pushing the image to ECR, you can create or update your Lambda function to use this image. This can be done through the AWS Console or using infrastructure as code tools like AWS CDK, CloudFormation, or Terraform.

Available Commands

Run just to see all available commands. Here are some key commands:

  • just up: Start all services with Docker Compose
  • just down: Stop and remove all containers
  • just rebuild: Rebuild Docker images
  • just build: Build the Docker image
  • just run: Run the Docker container
  • just ping: Test the Lambda function
  • just build-and-push: Build and push the Docker image to ECR
  • just list-ecr-images: List images in the ECR repository
  • just open-ecr-repository: Open the ECR repository in the browser

Development

Project Structure

auth0-authorizer/
├── app.py                 # Main Lambda handler
├── docker-compose.yml     # Docker Compose configuration
├── Dockerfile             # Docker configuration
├── justfile               # Just commands
├── poetry.lock            # Poetry lock file
├── pyproject.toml         # Project dependencies
├── README.md              # This file
└── utils/                 # Utility functions
    └── __init__.py        # Sentry setup and utilities

Dependencies

  • aws-lambda-powertools: Utilities for AWS Lambda functions
  • python-jose: JWT token validation
  • sentry-sdk: Error tracking and monitoring
  • boto3: AWS SDK for Python

Troubleshooting

If you encounter issues with the authorizer:

  1. Check that all environment variables are correctly set
  2. Verify that your Auth0 API is properly configured
  3. Ensure the JWT token is correctly formatted and not expired
  4. Check Sentry for detailed error reports
  5. Review CloudWatch logs for the Lambda function

License

MIT License

# File: app.py
from aws_lambda_powertools.utilities.typing import LambdaContext
from aws_lambda_powertools import Logger
import json
import os
import time
import urllib.request
from jose import jwk, jwt
from jose.utils import base64url_decode
from utils import sentry_message, setup_sentry
logger = Logger(service="lambda_auth0_jwt_authorizer")
setup_sentry()
"""
Auth0 JWT Authorizer for API Gateway
This Lambda function validates JWT tokens issued by Auth0
"""
# Environment variables
AUTH0_DOMAIN = os.environ.get('AUTH0_DOMAIN')
AUTH0_AUDIENCE = os.environ.get('AUTH0_AUDIENCE')
AUTH0_JWKS_URI = os.environ.get('AUTH0_JWKS_URI')
# Cache for JWKS
jwks_cache = {}
jwks_last_updated = 0
JWKS_CACHE_TTL = 3600 # 1 hour
def get_jwks():
"""
Fetch the JWKS from Auth0, with caching
"""
global jwks_cache, jwks_last_updated
current_time = time.time()
if jwks_cache and current_time - jwks_last_updated < JWKS_CACHE_TTL:
return jwks_cache
try:
with urllib.request.urlopen(AUTH0_JWKS_URI) as response:
jwks = json.loads(response.read().decode('utf-8'))
jwks_cache = jwks
jwks_last_updated = current_time
return jwks
except Exception as e:
print(f"Error fetching JWKS: {str(e)}")
raise Exception("Unauthorized")
def extract_token(auth_header):
"""
Extract token from Authorization header
"""
if not auth_header:
raise Exception("Authorization header is missing")
# Check if the header has the Bearer prefix
parts = auth_header.split()
if len(parts) != 2 or parts[0].lower() != 'bearer':
raise Exception("Authorization header format is invalid. Expected 'Bearer TOKEN'")
return parts[1]
def verify_token(token):
"""
Verify JWT token
"""
try:
# Decode the token header to get the kid (key id)
header = jwt.get_unverified_header(token)
if not header or 'kid' not in header:
raise Exception("Invalid token header")
# Get the JWKS
jwks = get_jwks()
# Find the signing key
rsa_key = None
for key in jwks['keys']:
if key['kid'] == header['kid']:
rsa_key = {
'kty': key['kty'],
'kid': key['kid'],
'use': key['use'],
'n': key['n'],
'e': key['e']
}
break
if not rsa_key:
raise Exception("Public key not found")
# Verify the token
payload = jwt.decode(
token,
rsa_key,
algorithms=['RS256'],
audience=AUTH0_AUDIENCE,
issuer=f'https://{AUTH0_DOMAIN}/'
)
return payload
except jwt.ExpiredSignatureError:
print("Token expired")
raise Exception("Token expired")
except jwt.JWTClaimsError:
print("Invalid claims")
raise Exception("Invalid claims")
except Exception as e:
print(f"Token verification failed: {str(e)}")
raise Exception("Unauthorized")
def generate_policy(principal_id, effect, resource, context=None):
"""
Generate IAM policy document
"""
policy = {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource
}
]
}
}
# Add context if provided
if context:
policy['context'] = context
return policy
@logger.inject_lambda_context(log_event=True)
def lambda_handler(event, context: LambdaContext):
"""
Lambda handler function
"""
logger.info("Lambda handler invoked with event:", event)
sentry_message("Lambda handler invoked", extra_context={'event': event})
try:
# Extract token from Authorization header
token = extract_token(event.get('authorizationToken', ''))
# Verify the token
payload = verify_token(token)
# Generate policy
policy = generate_policy(
payload['sub'],
'Allow',
event['methodArn'],
{
# Pass user claims to the API
'scope': payload.get('scope', ''),
'userId': payload['sub'],
'email': payload.get('email', '')
}
)
logger.info(json.dumps(policy, indent=2))
return policy
except Exception as e:
print(f"Authorization failed: {str(e)}")
print(f"Event: {event}")
print(f"Context: {context}")
# For security, return a generic error message
raise Exception("Unauthorized")
name: authorizer
services:
authorizer:
container_name: authorizer
image: authorizer:${DOCKER_TAG:-local}
command: [ "app.lambda_handler" ]
build:
context: ./
dockerfile: Dockerfile
volumes:
- .:/var/task/
ports:
- '9030:8080'
# Use the official Python image as a base
FROM public.ecr.aws/lambda/python:3.12
# Set the working directory
WORKDIR /var/task
# Install Poetry
RUN pip install --no-cache-dir poetry
# Copy Poetry configuration files
COPY pyproject.toml poetry.lock* /var/task/
# Install dependencies
RUN poetry config virtualenvs.create false && poetry install --only main --no-interaction --no-ansi
# Copy application code
COPY app.py /var/task/
# Set the Lambda function handler
CMD ["app.lambda_handler"]
[tool.poetry]
name = "authorizer"
version = "0.1.0"
description = "custom authorizer"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
aws-lambda-powertools = "^1.30.0"
python-jose = "^3.4.0"
sentry-sdk = "^2.25.1"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment