Lambda Authorizers
Token validation at the API Gateway edge — request vs token types, JWT verification, caching, and when to use Lambda authorizers over Cognito or IAM auth.
- DATE:
- JUN.04.2025
- READ:
- 10 MIN
Why authorize at the gateway
Every request that reaches your backend Lambda costs compute time. If that request is unauthorized, you’ve paid for a cold start, function execution, and downstream calls — all to return a 403. Lambda authorizers move the rejection upstream. API Gateway evaluates credentials before invoking your integration, so unauthorized requests never touch your business logic.
This isn’t just about cost. It’s about attack surface. Your backend code doesn’t need to import JWT libraries, manage JWKS caching, or handle token expiry edge cases. API Gateway already handles TLS termination, request throttling, and WAF integration. Adding authorization at the same layer keeps the security boundary tight and the backend focused on what it exists to do — serve business logic.
The authorizer function returns an IAM policy document. API Gateway caches and enforces it. Your backend receives the request only if the policy allows execute-api:Invoke on the target resource.
Token vs Request authorizers
API Gateway supports two authorizer types. They differ in what context the authorizer Lambda receives.
A TOKEN authorizer receives a single identity source — typically the Authorization header. API Gateway extracts the token value and passes it to your Lambda as event.authorizationToken. You optionally define a token validation regex that Gateway checks before invoking the Lambda, rejecting obviously malformed tokens without a function call.
A REQUEST authorizer receives the full request context: headers, query string parameters, stage variables, and the caller’s source IP. The identity comes from whichever combination of these sources you configure.
+--------------------+--------------------+--------------------+ | Aspect | TOKEN authorizer | REQUEST authorizer | +--------------------+--------------------+--------------------+ | Identity source | Single header | Headers, query | | | (bearer token) | strings, stage | | | | variables, IP | +--------------------+--------------------+--------------------+ | Cache key | Token value only | All configured | | | | identity sources | +--------------------+--------------------+--------------------+ | Validation regex | Supported | Not supported | | | (pre-invocation | | | | check) | | +--------------------+--------------------+--------------------+ | Multi-source auth | No | Yes (e.g. token + | | | | API key + IP) | +--------------------+--------------------+--------------------+ | AWS recommendation | Legacy default | Recommended for | | | | new APIs | +--------------------+--------------------+--------------------+ | HTTP API (v2) | Not available | Supported | +--------------------+--------------------+--------------------+
AWS recommends REQUEST authorizers as the default for new APIs. The cache key flexibility alone justifies it — TOKEN authorizers cache solely on the token value, so changing a user’s IP or adding a query-level scope doesn’t affect cache lookup. REQUEST authorizers include all identity sources in the key, producing more precise cache behavior.
The JWT validation pattern
The authorizer Lambda receives the token, validates it against the identity provider’s JWKS endpoint, and returns an IAM policy document. Here’s the standard pattern for RS256-signed JWTs:
import json, jwt, urllib.request
JWKS_URL = "https://auth.acmecorp.com/.well-known/jwks.json"
AUDIENCE = "https://api.acmecorp.com"
ISSUER = "https://auth.acmecorp.com/"
# Cache JWKS in execution context
_jwks_cache = None
def get_jwks():
global _jwks_cache
if _jwks_cache is None:
with urllib.request.urlopen(JWKS_URL) as r:
_jwks_cache = json.loads(r.read())
return _jwks_cache
def handler(event, context):
token = event.get("authorizationToken", "").replace("Bearer ", "")
try:
header = jwt.get_unverified_header(token)
jwks = get_jwks()
key = next(k for k in jwks["keys"] if k["kid"] == header["kid"])
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
claims = jwt.decode(
token, public_key,
algorithms=["RS256"],
audience=AUDIENCE,
issuer=ISSUER,
)
return generate_policy(claims["sub"], "Allow", event["methodArn"])
except Exception:
return generate_policy("anonymous", "Deny", event["methodArn"])
def generate_policy(principal, effect, resource):
return {
"principalId": principal,
"policyDocument": {
"Version": "2012-10-17",
"Statement": [{
"Action": "execute-api:Invoke",
"Effect": effect,
"Resource": resource,
}]
}
}Three things to note. First, _jwks_cache lives outside the handler — it persists across warm invocations in the same execution context, avoiding a network call to the JWKS endpoint on every request. Second, the kid (key ID) from the token header selects the correct signing key from the JWKS set. Key rotation at the IdP adds a new key with a new kid without breaking existing tokens. Third, jwt.decode validates the signature, expiry (exp), audience (aud), and issuer (iss) in a single call. If any check fails, the except block returns a Deny policy.
CDK wiring
The authorizer Lambda and its attachment to API Gateway resources are straightforward CDK constructs:
authorizer_fn = _lambda.Function(
self, "AuthorizerFunction",
runtime=_lambda.Runtime.PYTHON_3_12,
handler="index.handler",
code=_lambda.Code.from_asset("lambda/authorizer"),
timeout=Duration.seconds(10),
memory_size=128,
)
authorizer = apigw.TokenAuthorizer(
self, "JwtAuthorizer",
handler=authorizer_fn,
results_cache_ttl=Duration.minutes(5),
)
api.root.add_resource("users").add_method(
"GET", lambda_integration,
authorizer=authorizer,
)results_cache_ttl controls how long API Gateway caches the authorizer’s policy response. Setting it to Duration.minutes(5) means a valid token triggers the authorizer Lambda once, then the cached Allow policy serves subsequent requests for five minutes. For REQUEST authorizers, use apigw.RequestAuthorizer and specify identity_sources to define the cache key components.
Caching
API Gateway caches the authorizer response — the entire IAM policy document — keyed on the identity source. The TTL ranges from 0 to 3600 seconds, with a default of 300 (5 minutes).
For TOKEN authorizers, the cache key is the token value itself. Same token, same cached policy. For REQUEST authorizers, the cache key combines all configured identity sources. If you specify the Authorization header and a clientId query parameter as identity sources, both values must match for a cache hit.
When the cache is hit, the authorizer Lambda is not invoked. This is the primary performance benefit — the authorizer cold start is eliminated for repeat requests within the TTL window.
Set TTL to 0 during development. Cached policies make debugging authorization logic painful — you change the Lambda code, redeploy, and the old policy keeps getting served.
HTTP API v2 simple response
HTTP APIs (API Gateway v2) simplify the authorizer contract. Instead of returning an IAM policy document, your Lambda can return a simple boolean response:
{
"isAuthorized": true,
"context": {
"userId": "user-123",
"scope": "read:users"
}
}No policyDocument, no principalId, no execute-api:Invoke boilerplate. The context object passes claims downstream — your backend Lambda receives them in event.requestContext.authorizer.lambda.
HTTP APIs also support native JWT authorizers with zero Lambda code. You configure the issuer URL and audience directly on the authorizer resource, and API Gateway validates the JWT internally. This works for standard OIDC providers (Auth0, Okta, Cognito) where you only need signature, expiry, and audience validation — no custom claim logic.
Comparison
Choosing the right auth mechanism depends on your identity provider, API type, and complexity requirements.
+--------------------+--------------------+--------------------+ | Auth type | Best for | Drawback | +--------------------+--------------------+--------------------+ | Lambda Authorizer | Custom IdP, | Cold start latency | | | complex claim | | | | logic | | +--------------------+--------------------+--------------------+ | Cognito User Pool | Cognito-only | REST API only, | | | applications | Cognito lock-in | +--------------------+--------------------+--------------------+ | IAM Auth (SigV4) | Service-to-service | Callers need AWS | | | calls | credentials | +--------------------+--------------------+--------------------+ | JWT Authorizer | Standard JWT | HTTP API only | | (v2) | validation | | +--------------------+--------------------+--------------------+
Lambda authorizers win when you need custom logic — checking database-backed permissions, validating tokens from a non-standard IdP, or combining multiple identity sources. Cognito authorizers are simpler but lock you into the Cognito ecosystem and only work with REST APIs (v1). IAM auth is the right choice for internal service-to-service communication where both sides have AWS credentials. JWT authorizers on HTTP APIs cover the common case of standard OIDC token validation without any Lambda overhead.
Performance
The authorizer Lambda sits in the critical path of every API request. A cold start on the authorizer adds latency before the backend Lambda even begins its own cold start. Two sequential cold starts compound into noticeable delays.
Mitigations, in order of impact:
- Caching — The single biggest improvement. A 5-minute TTL eliminates the authorizer invocation entirely for repeat requests. Match the TTL to your token’s
expclaim duration for optimal coverage. - JWKS caching in execution context — Store the JWKS response in a module-level variable (as shown above). The JWKS endpoint doesn’t change on every request — keys rotate on the order of hours or days.
- Provisioned concurrency — Eliminate cold starts entirely by keeping warm execution environments. Costs more, but guarantees consistent latency. Apply to the authorizer Lambda independently of the backend.
- Stay out of VPC — The authorizer Lambda rarely needs VPC access. It validates tokens against a public JWKS endpoint. Placing it in a VPC adds ENI attachment time to every cold start.
- Minimize the deployment package — The
PyJWTlibrary withcryptographyis all you need. Don’t bundle your entire application’s dependencies into the authorizer.
Authorization at the gateway is a force multiplier — one Lambda function protects every route behind it. Get the caching right, keep the function lean, and your backend never has to think about tokens again.