Appearance
Solution 1: Migrate to REST API Gateway with Lambda Authorizer Caching
Overview
- REST API Gateway supports caching Lambda Authorizer results, which significantly reduces repeat authorization lookups.
- Cache keys are based on the combination of the JWT token, API path, and HTTP method.
- When a cache hit occurs, the authorizer and DynamoDB are not invoked again.
Architecture (Solution 1)
text
Client
|
|---> REST API Gateway
|
|---> Lambda Authorizer (invoked once to check permissions)
| |
| +--> Caches authorization result (token + path + method)
|
+---> Your Backend APIDevelopment Steps
Switch from HTTP API Gateway to REST API Gateway
Configure Lambda Authorizer:
identitySource:"method.request.header.Authorization"authorizerResultTtlInSeconds:300(or 60–3600 seconds based on your need)
Implement Lambda Authorizer logic:
- Decode JWT
- Look up the user’s permissions from DynamoDB
- Return IAM policy document with access rules
REST API Gateway uses the policy to allow or deny access
Pros
- High performance (no DynamoDB access on cache hit)
- Fine-grained control per endpoint
- Officially recommended by AWS
Cons
- Requires migration from HTTP to REST API Gateway (added complexity)
- Cache is per token + path + method, reducing hit rates on dynamic paths
- First-time requests to many endpoints will still hit DynamoDB
Solution 2: Embed Permissions in JWT Using Pre-Token Trigger
Overview
- During login, a Cognito Pre Token Generation Lambda Trigger injects user role/permission data into the JWT (
custom:*claims). - Lambda Authorizer simply decodes the JWT and performs permission checks directly—no DynamoDB involved.
Architecture (Solution 2)
Client
|
|---> API Gateway (HTTP or REST)
|
|---> Lambda Authorizer (JWT decoding only)
| |
| +--> Checks permissions from JWT claims
|
+---> Your Backend APIDevelopment Steps
Set up Cognito User Pool’s Pre Token Generation trigger:
- The Lambda fetches permission data from DynamoDB
- Injects
custom:role,custom:permissionsinto JWT
API Gateway verifies JWT signature (Cognito Authorizer or Lambda Authorizer)
Lambda Authorizer decodes JWT and checks access permissions using claims
Pros
- Extremely fast (no DynamoDB or Lambda invocation needed on cache hit)
- Works with both HTTP and REST API Gateway
- Simple and highly responsive
Cons
- User must re-login for updated permissions to reflect (or refresh the token)
- JWT size grows if too many permissions are embedded
- Security risk if tokens are compromised—set token TTL appropriately (e.g. 15–60 min)
Solution 4: Authorization at Backend (Microservice-Level RBAC)
Overview
- API Gateway only verifies JWT signature (authentication)
- Backend service decodes the token and checks whether the user is allowed to access the resource
- All authorization logic lives in your backend
Architecture (Solution 4)
Client
|
|---> API Gateway (HTTP or REST)
|
+---> Backend API
|
+---> Decode JWT
+---> Check permissions (DynamoDB or Redis)Development Steps
Configure API Gateway to verify JWT (Cognito Authorizer)
On each backend request:
- Decode the token
- Extract user ID, role, or permissions
- Check permissions against DynamoDB, Redis, or in-memory cache
Return 403 Forbidden if access is denied
Pros
- Very flexible, supports complex business rules and multi-level permission control
- Not dependent on API Gateway features or limitations
- Easy to integrate into microservice or RBAC/ABAC systems
Cons
- Authorization logic is decentralized—harder to maintain across services
- Developers need to implement and manage caching (e.g. Redis)
- Larger systems may suffer from permission management fragmentation
Summary Comparison
| Solution | Gateway Type | Auth Logic Location | Caching Support | Pros | Ideal Use Case |
|---|---|---|---|---|---|
| 1. REST + Cached Lambda | REST API Gateway | Lambda Authorizer | TTL-based caching | AWS standard, scalable | Mid to large systems |
| 2. JWT with Claims | HTTP or REST | Lambda or Inline Authorizer | JWT is self-contained | Fastest, simplest | Stable permissions |
| 4. Backend Authorization | HTTP or REST | Microservice / Backend | Optional Redis or memory | Flexible, business-driven | Complex permission logic |
Recommended Hybrid Strategy
For most production-grade systems:
- Use Solution 2 (JWT with embedded permissions).
- Use Solution 4 for sensitive actions that require real-time permission checks or complex business logic.
POC sample
Python-based example covering two core parts of the solution:
Part 1: Cognito Pre Token Generation Lambda (Inject Permissions into JWT)
This Lambda function is triggered by Cognito before token issuance, and adds custom claims like custom:role and custom:permissions to the token by querying DynamoDB.
python
import boto3
dynamodb = boto3.resource('dynamodb')
permissions_table = dynamodb.Table('UserPermissions')
def lambda_handler(event, context):
user_id = event['request']['userAttributes']['sub']
# Query DynamoDB for user's permissions
response = permissions_table.get_item(Key={'userId': user_id})
permissions = response.get('Item', {}).get('permissions', [])
role = response.get('Item', {}).get('role', 'guest')
# Inject custom claims into token
event['response']['claimsOverrideDetails'] = {
'claimsToAddOrOverride': {
'custom:role': role,
'custom:permissions': ','.join(permissions)
}
}
return eventAttach this Lambda to the Pre Token Generation trigger in the Cognito User Pool.
Part 2: Lambda Authorizer for API Gateway (Verify Permissions from JWT)
This is a Lambda Authorizer used by API Gateway (REST or HTTP) to extract the JWT, parse custom claims, and return access policies.
python
import jwt
# This should be the Cognito User Pool's public key (retrieved and cached from JWKs endpoint)
COGNITO_JWT_SECRET = '$$$$' # Placeholder
def lambda_handler(event, context):
token = event['headers'].get('Authorization', '').replace('Bearer ', '')
try:
decoded = jwt.decode(token, COGNITO_JWT_SECRET, algorithms=['RS256'], options={"verify_aud": False})
user_permissions = decoded.get('custom:permissions', '').split(',')
path = event['rawPath']
method = event['requestContext']['http']['method']
# Example permission check
if f"{method}:{path}" not in user_permissions:
raise Exception("Permission Denied")
return {
"isAuthorized": True,
"context": {
"user": decoded.get("sub"),
"role": decoded.get("custom:role")
}
}
except Exception as e:
print(f"Authorization error: {str(e)}")
return {
"isAuthorized": False,
"context": {}
}Example JWT custom:* Claims Structure:
json
{
"sub": "abc123",
"custom:role": "admin",
"custom:permissions": "GET:/api/users,POST:/api/reports,GET:/api/devices"
}