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:

  1. Client sends request: The client includes a JWT in the Authorization header, like Authorization: Bearer eyJ....
  2. API Gateway intercepts: API Gateway sees the request and its configured authorizer.
  3. Authorizer Lambda is invoked: API Gateway calls your JWT authorizer Lambda function, passing the JWT (usually in the Authorization header).
  4. 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.
  5. Authorizer Lambda returns policy: Based on the validation, the authorizer Lambda returns an IAM policy to API Gateway. This policy specifies whether to Allow or Deny access to the requested API resource and any specific methods.
  6. 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 Unauthorized or 403 Forbidden to 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.

Want structured learning?

Take the full Apigateway course →