Securing Serverless APIs: A Guide to Lambda Authorizers
Learn how to secure your Amazon API Gateway endpoints using AWS Lambda authorizers, with a focus on validating JWTs (JSON Web Tokens) for robust, token-based authentication.
When you build a public-facing API with Amazon API Gateway and AWS Lambda, one of the first questions you must answer is: "How do I control who can access it?" While API keys offer basic protection, a much more robust and standard approach is to use token-based authentication with JSON Web Tokens (JWTs).
API Gateway's Lambda Authorizers (formerly known as Custom Authorizers) are the perfect tool for this job. They are Lambda functions that you write to implement your own custom authorization logic.
How Do Lambda Authorizers Work?
The flow is simple but powerful:
- A client makes a request to your API Gateway endpoint, including an
Authorization
header (e.g.,Bearer eyJhbGciOi...
). - API Gateway automatically invokes your Lambda Authorizer function before forwarding the request to your backend integration (e.g., your main Lambda function).
- It passes the entire
Authorization
token to the authorizer function. - Your authorizer function's job is to validate the token. If the token is valid, the function returns an IAM policy document that either allows or denies the request.
- If allowed, API Gateway proceeds to invoke your backend function. If denied, it returns a
403 Forbidden
error to the client.
Building a JWT Lambda Authorizer
Let's build an authorizer in Python that validates a JWT signed with the RS256 algorithm, a common standard used by identity providers like Auth0 or Amazon Cognito.
Prerequisites:
- An identity provider (like Cognito) that issues JWTs.
- The public key (JWKS - JSON Web Key Set) URL of your identity provider.
The Authorizer Code:
import os
import jwt
import requests
from functools import lru_cache
# Identity Provider's JWKS URL - store this in an environment variable
JWKS_URL = os.environ.get('JWKS_URL')
@lru_cache(maxsize=1)
def get_jwks():
"""Fetches the JWKS and caches it."""
return requests.get(JWKS_URL).json()
def get_public_key(token):
"""Finds the appropriate public key from the JWKS to verify the token."""
try:
unverified_header = jwt.get_unverified_header(token)
except jwt.exceptions.DecodeError:
raise ValueError('Invalid token header')
jwks = get_jwks()
for key in jwks['keys']:
if key['kid'] == unverified_header['kid']:
return {
'kty': key['kty'],
'kid': key['kid'],
'use': key['use'],
'n': key['n'],
'e': key['e']
}
raise ValueError('Public key not found')
def handler(event, context):
"""The Lambda Authorizer main handler."""
try:
token = event['authorizationToken'].split(' ')[1] # Extract token from 'Bearer <token>'
public_key = get_public_key(token)
# Decode and verify the token
decoded = jwt.decode(
token,
public_key,
algorithms=['RS256'],
audience='my-api-audience', # The audience for your API
issuer='https://my-idp.com/' # The issuer from your IDP
)
# If verification is successful, return an 'Allow' policy
policy = generate_policy(decoded['sub'], 'Allow', event['methodArn'])
# You can also pass context to the backend Lambda
policy['context'] = {
'userId': decoded['sub'],
'scope': decoded.get('scope', '')
}
return policy
except (ValueError, jwt.PyJWTError) as e:
print(f"Auth error: {e}")
# For security, don't return detailed error messages to the client
# API Gateway will automatically return a 403 Forbidden
return generate_policy('user', 'Deny', event['methodArn'])
def generate_policy(principal_id, effect, resource):
"""Generates the required IAM policy document."""
return {
'principalId': principal_id,
'policyDocument': {
'Version': '2012-10-17',
'Statement': [
{
'Action': 'execute-api:Invoke',
'Effect': effect,
'Resource': resource,
}
],
},
}
Key Concepts in the Code
- JWKS (JSON Web Key Set): This is a standard way for an identity provider to publish its public keys. Our authorizer fetches this set to find the correct key to verify the token's signature.
- Caching (
@lru_cache
): Fetching the JWKS is an I/O operation. Caching it in memory significantly improves the performance of subsequent authorizer invocations. - Token Validation: We're not just checking the signature. We are also validating the
audience
(is this token intended for our API?) and theissuer
(did it come from the identity provider we trust?). - Policy Generation: The final output must be an IAM policy. This is how you tell API Gateway whether the request is authorized.
- Passing Context: The
context
block in the returned policy is a powerful feature. Any key-value pairs you add here will be passed directly to your backend Lambda function in theevent.requestContext.authorizer
object. This is perfect for passing the user's ID or permissions without needing to decode the JWT again in your business logic.
Conclusion
Lambda Authorizers are a flexible and powerful way to implement custom security logic for your serverless APIs. By leveraging them to validate JWTs, you can build secure, scalable, and standards-compliant authentication systems that integrate seamlessly with any OpenID Connect (OIDC) compliant identity provider.