Skip to content

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 API

Development Steps

  1. Switch from HTTP API Gateway to REST API Gateway

  2. Configure Lambda Authorizer:

    • identitySource: "method.request.header.Authorization"
    • authorizerResultTtlInSeconds: 300 (or 60–3600 seconds based on your need)
  3. Implement Lambda Authorizer logic:

    • Decode JWT
    • Look up the user’s permissions from DynamoDB
    • Return IAM policy document with access rules
  4. 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 API

Development Steps

  1. Set up Cognito User Pool’s Pre Token Generation trigger:

    • The Lambda fetches permission data from DynamoDB
    • Injects custom:role, custom:permissions into JWT
  2. API Gateway verifies JWT signature (Cognito Authorizer or Lambda Authorizer)

  3. 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

  1. Configure API Gateway to verify JWT (Cognito Authorizer)

  2. On each backend request:

    • Decode the token
    • Extract user ID, role, or permissions
    • Check permissions against DynamoDB, Redis, or in-memory cache
  3. 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

SolutionGateway TypeAuth Logic LocationCaching SupportProsIdeal Use Case
1. REST + Cached LambdaREST API GatewayLambda AuthorizerTTL-based cachingAWS standard, scalableMid to large systems
2. JWT with ClaimsHTTP or RESTLambda or Inline AuthorizerJWT is self-containedFastest, simplestStable permissions
4. Backend AuthorizationHTTP or RESTMicroservice / BackendOptional Redis or memoryFlexible, business-drivenComplex permission logic

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 event

Attach 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"
}