A Lambda TOKEN authorizer handler that protects an Amazon API Gateway REST API by validating the OAuth 2.0 / OIDC access_token presented in the request. It works with any OpenID Connect (OIDC)-compliant identity provider and has been specifically tested with Microsoft Entra ID (Azure AD) v2.0.
Unlike the CloudFront edge handler, this handler performs bearer-token validation only — it does not implement the login flow, PKCE, cookies, or token refresh. It assumes the caller already holds a valid access_token (obtained out of band) and passes it in the Authorization header. The handler returns an IAM policy that API Gateway uses to allow or deny the invocation.
API Gateway invokes the authorizer with the value of the request's Authorization header (the authorizationToken) and the methodArn being invoked. The handler verifies the token and returns an Allow or Deny IAM policy.
sequenceDiagram
actor Client
participant APIGW as API Gateway
participant Auth as Authorizer Lambda
participant IDP as Identity Provider<br/>(Entra ID)
participant Backend
Note over Client,Backend: 1 — First request with a token (cold authorizer)
Client->>APIGW: GET /resource<br/>Authorization: Bearer <access_token>
APIGW->>Auth: authorizationToken + methodArn
Auth->>IDP: GET /.well-known/openid-configuration
IDP-->>Auth: token_endpoint, jwks_uri, issuer
Auth->>IDP: GET jwks_uri (public keys)
IDP-->>Auth: JWKS
Note over Auth: Verify JWT (signature, aud, iss, expiry)
Auth-->>APIGW: Allow policy (stage-wide wildcard)
APIGW->>Backend: Forwarded request
Backend-->>Client: Response
Note over Client,Backend: 2 — Subsequent request (warm authorizer, cached policy)
Client->>APIGW: GET /other-resource<br/>Authorization: Bearer <same access_token>
Note over APIGW: Cached authorizer result reused<br/>(policy covers the whole stage)
APIGW->>Backend: Forwarded request
Backend-->>Client: Response
Note over Client,Backend: 3 — Invalid or missing token
Client->>APIGW: GET /resource<br/>Authorization: <invalid / none>
APIGW->>Auth: authorizationToken + methodArn
Note over Auth: Verification fails
Auth-->>APIGW: Deny policy (context.message = reason)
APIGW-->>Client: 403 ForbiddenThe authorizationToken is taken verbatim from the request's Authorization header. A leading Bearer prefix is stripped if present; otherwise the whole header value is treated as the token. If no token is present, the handler returns a Deny policy with the message No token provided.
The access_token is verified against the IDP's published JWKS (jwks_uri from the OIDC discovery document). Verification checks:
RS256)clientId plus any values in audienceissuer from the OIDC config plus any values in the issuer optionexp validation)On success the handler builds an Allow policy whose principalId is the token's upn claim. On failure it returns a Deny policy whose context.message carries the verification error message.
API Gateway caches a TOKEN authorizer's result per token, and reuses it across every method and resource path invoked with the same token. If the returned policy were scoped to the single methodArn that triggered the authorizer, the cached policy would be rejected on any other method invoked with the same token.
To avoid this, a successful policy is scoped to a stage-wide wildcard:
arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/{method}/{path}
↓
arn:aws:execute-api:{region}:{account}:{apiId}/{stage}/*/*
A Deny policy is left scoped to the original methodArn — denials are not meant to be cached and reused across the stage.
The handler keeps an in-memory cache (default TTL: 60 minutes, see utils/cache.ts) across warm invocations for:
This minimises IDP round-trips: only the first invocation on a cold container fetches the discovery document and keys.
Configuration is passed to the CDK construct and injected into the Lambda as environment variables. The handler reads them via getOpts():
| Environment variable | Source option | Description |
|---|---|---|
CLIENT_ID |
clientId |
OAuth 2.0 client ID; always an accepted aud claim value. |
WELL_KNOWN_URI |
wellKnownUri |
OIDC discovery document URL. |
IDP_AUDIENCE |
audience |
JSON array of additional accepted aud claim values. |
IDP_ISSUER |
issuer |
JSON array of additional accepted iss claim values. |
OAuthOptions| Option | Type | Required | Description |
|---|---|---|---|
clientId |
string |
✓ | The OAuth 2.0 / Entra ID application (client) ID. Validated as the aud claim by default. For Entra ID this is the Enterprise App ID of the OIDC configuration. |
wellKnownUri |
string |
✓ | URL of the OIDC discovery document. For Entra ID: https://login.microsoftonline.com/{tenantId}/v2.0/.well-known/openid-configuration |
audience |
string[] |
— | Additional aud claim values accepted during access_token validation. Extends the default — clientId is always accepted. Use when the IDP issues access tokens scoped to a resource URI rather than the client ID. Example: ['api://{resourceId}'] |
issuer |
string[] |
— | Additional iss claim values, merged with the issuer read from the well-known endpoint. Required for Entra ID when the app manifest's accessTokenAcceptedVersion is null or 1 — the access token issuer is then https://sts.windows.net/{tenantId}/ rather than the v2 issuer. |
Entra ID issues two structurally different access_token formats depending on the app registration configuration:
accessTokenAcceptedVersion in app manifest |
Access token iss claim |
issuer option required? |
|---|---|---|
null or 1 (default) |
https://sts.windows.net/{tenantId}/ |
Yes |
2 |
https://login.microsoftonline.com/{tenantId}/v2.0 |
No (matches OIDC discovery) |
When using v1 access tokens (the default for most tenants), you must set the issuer option to the STS issuer URL, otherwise every verification call will fail with an issuer mismatch.
The principalId of a successful policy is taken from the token's upn (User Principal Name) claim. Entra ID includes it in access_tokens for user identities; it may be absent for guest accounts, service principals, or v1 tokens, in which case the principalId is an empty string but the request is still allowed.
Deny policy, context.message = 'No token provided'.Deny policy scoped to the methodArn, with the error message in context.message. API Gateway translates this into a 403 Forbidden.Deny).All fetch calls (OIDC discovery and JWKS retrieval) use an AbortController with a timeout derived from the Lambda's remaining execution time minus a 500 ms grace period (TIMEOUT_GRACE), so the handler fails gracefully rather than hanging until the Lambda hard timeout.
Logging: the handler logs the raw event and config only at
DEBUGlevel, which exposes the access token. Keep the log level atINFO(the default) outside of test environments.