A JWT authorizer for API Gateway doesn’t actually validate the JWT itself; it delegates that to a separate service.
Let’s see how this plays out. Imagine a user requests GET /users/123 on your API. API Gateway receives this request. Before it even thinks about forwarding it to your Lambda function that handles user data, it needs to know if the request is allowed. That’s where the JWT authorizer comes in.
Here’s a simplified flow:
- Client sends request: The client includes a JWT in the
Authorizationheader, likeAuthorization: Bearer eyJ.... - API Gateway intercepts: API Gateway sees the request and its configured authorizer.
- Authorizer Lambda is invoked: API Gateway calls your JWT authorizer Lambda function, passing the JWT (usually in the
Authorizationheader). - Authorizer Lambda validates (or doesn’t): This is the core part. Your authorizer Lambda should inspect the JWT. It checks the signature, expiration, issuer, and audience against known trusted values.
- Authorizer Lambda returns policy: Based on the validation, the authorizer Lambda returns an IAM policy to API Gateway. This policy specifies whether to
AlloworDenyaccess to the requested API resource and any specific methods. - API Gateway enforces policy: If the policy allows access, API Gateway forwards the request to your backend integration (e.g., another Lambda function). If denied, it returns a
401 Unauthorizedor403 Forbiddento the client.
Crucially, the authorizer Lambda also has the option to pass context to the backend. If the JWT is valid, it can extract claims (like userId, tenantId, roles) and return them as context in the IAM policy. Your backend Lambda can then access these claims via the event.requestContext.authorizer.claims object.
To build this, you’ll need:
- An API Gateway REST API or HTTP API: The entry point for your requests.
- A Lambda function for your authorizer: This is where the JWT validation logic lives.
- A JWT issuer: Somewhere issuing the tokens (e.g., Auth0, Okta, Cognito, or a custom service).
- A mechanism for your authorizer Lambda to get the public keys or JWKS endpoint of the issuer: This is how it verifies the signature.
Let’s look at a basic Python authorizer Lambda. We’ll use the PyJWT library for validation.
import json
import jwt
import os
import urllib.request
# Get JWKS endpoint from environment variable
# Example: https://your-auth-provider.com/.well-known/jwks.json
JWKS_URL = os.environ.get("JWKS_URL")
EXPECTED_AUDIENCE = os.environ.get("EXPECTED_AUDIENCE", "api://my-api")
EXPECTED_ISSUER = os.environ.get("EXPECTED_ISSUER", "https://your-auth-provider.com/")
def get_jwks():
"""Fetches the JWKS from the configured URL."""
try:
with urllib.request.urlopen(JWKS_URL) as response:
return json.loads(response.read())
except Exception as e:
print(f"Error fetching JWKS: {e}")
raise
def generate_policy(principal_id, effect, resource, context=None):
"""Generates an IAM policy document."""
policy = {
"principalId": principal_id,
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": effect,
"Resource": resource
}
]
}
}
if context:
policy["context"] = context
return policy
def lambda_handler(event, context):
"""
Lambda authorizer function.
Validates a JWT and returns an IAM policy.
"""
token = event.get("authorizationToken")
if not token:
raise Exception("Unauthorized: Missing authorization token")
if not token.startswith("Bearer "):
raise Exception("Unauthorized: Token must be Bearer type")
token_value = token.split(" ")[1]
try:
# 1. Fetch JWKS
jwks = get_jwks()
public_key = None
header = jwt.get_unverified_header(token_value)
kid = header.get("kid") # Key ID from the JWT header
if not kid:
raise Exception("Unauthorized: Missing 'kid' in JWT header")
# Find the public key corresponding to the kid
for jwk in jwks["keys"]:
if jwk["kid"] == kid:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk))
break
if not public_key:
raise Exception("Unauthorized: Public key not found for kid")
# 2. Decode and validate the JWT
decoded_token = jwt.decode(
token_value,
public_key,
algorithms=["RS256"], # Ensure this matches your issuer's algorithm
audience=EXPECTED_AUDIENCE,
issuer=EXPECTED_ISSUER
)
# 3. Extract claims and generate policy
user_id = decoded_token.get("sub") # 'sub' is standard for subject/user ID
if not user_id:
raise Exception("Unauthorized: Missing 'sub' claim in JWT")
# Pass relevant claims to the backend
authorizer_context = {
"userId": user_id,
"tenantId": decoded_token.get("tenant_id", "default"), # Example: custom claim
"roles": ",".join(decoded_token.get("roles", ["user"])) # Example: comma-separated roles
}
# Allow access to the requested resource
# The resource ARN is passed in event['methodArn']
return generate_policy(user_id, "Allow", event["methodArn"], authorizer_context)
except jwt.ExpiredSignatureError:
print("Token has expired")
raise Exception("Unauthorized: Token expired")
except jwt.InvalidAudienceError:
print("Invalid audience")
raise Exception("Unauthorized: Invalid audience")
except jwt.InvalidIssuerError:
print("Invalid issuer")
raise Exception("Unauthorized: Invalid issuer")
except jwt.InvalidTokenError as e:
print(f"Invalid token: {e}")
raise Exception("Unauthorized: Invalid token")
except Exception as e:
print(f"Error during authorization: {e}")
raise Exception("Unauthorized: Internal error")
When configuring this Lambda in API Gateway, you’ll set up an "Authorizer" resource. You’ll specify the Lambda function, the "Token Source" (usually Authorization header), and potentially "Token Validation" if you were doing custom regex matching (though PyJWT handles the heavy lifting). You’ll also define "Authorization Scopes" if your JWT uses OAuth scopes for finer-grained access control.
The EXPECTED_AUDIENCE and EXPECTED_ISSUER environment variables are critical. The audience identifies your API, and the issuer identifies who issued the token. Mismatches here are common failure points. The JWKS URL is how your authorizer fetches the public keys needed to verify the JWT’s signature.
The most surprising thing about JWT authorizers is how much of the "validation" is actually just configuration in API Gateway itself, not code in your authorizer Lambda. API Gateway can be configured to automatically extract JWTs from headers or query parameters, pass them to your Lambda, and then use the returned policy.
Consider a request hitting your API Gateway: GET /items. The client has a JWT.
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij..." \
https://your-api-id.execute-api.us-east-1.amazonaws.com/prod/items
API Gateway receives this. It has an authorizer configured for the /items route. This authorizer is your Lambda function. API Gateway extracts Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij... from the Authorization header. It then invokes your authorizer Lambda, passing an event object like this:
{
"type": "TOKEN",
"methodArn": "arn:aws:execute-api:us-east-1:123456789012:your-api-id/prod/GET/items",
"authorizationToken": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij...",
"requestContext": {
"identity": {
"apiKey": null,
"sourceIp": "1.2.3.4"
}
}
}
Your Lambda, as shown above, processes this, validates the token, and returns a policy. If it returns:
{
"principalId": "auth0|user123",
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": "arn:aws:execute-api:us-east-1:123456789012:your-api-id/prod/GET/items"
}
]
},
"context": {
"userId": "auth0|user123",
"tenantId": "acme",
"roles": "admin,user"
}
}
API Gateway then forwards the original request to your backend GET /items Lambda, and that Lambda function will receive an event object containing event['requestContext']['authorizer']['claims']['userId'] (which would be "auth0|user123"), tenantId ("acme"), and roles ("admin,user").
One subtle but powerful aspect is the ability for the authorizer Lambda to cache policies. API Gateway can cache the policy returned by your authorizer based on the authorizationToken for a configurable TTL (Time To Live). This significantly reduces the number of times your authorizer Lambda is invoked, improving performance and reducing cost. You control this caching behavior in the API Gateway authorizer configuration settings. If you don’t explicitly disable it, API Gateway will cache.
The next hurdle is handling different token types or implementing more complex authorization logic beyond simple JWT validation.