The situation
A payments platform runs three APIs on API Gateway (a REST API and two HTTP APIs, for reasons that are historical and not interesting) and needs to answer, per endpoint, the question: how does this endpoint decide whether the caller is allowed?
Four caller profiles are in play.
- Mobile app consumers. End users on their phones with a Cognito user pool login. Need to identify the user per request and authorise based on the user’s group memberships.
- Third-party partners. Registered integrations with an account-manager-assigned API key. Usage plan applies throttle limits and daily quotas.
- Internal AWS callers. Other Lambdas and ECS tasks in the same AWS account that need to call the API. Should authenticate as their IAM role, not with a user credential.
- Bank’s B2B back-office. External service that signs requests with a short-lived JWT issued by the bank’s own OIDC provider. Claims inside the JWT determine what the caller can do.
Four profiles, and API Gateway’s authoriser options line up surprisingly cleanly if we understand what each type does.
What actually matters
Before reaching for an authoriser type, it’s worth asking what we’re actually asking an authoriser to do.
The first thing worth thinking about is who owns the identity provider. A Cognito user pool is ours; we control the claims, the password policy, the MFA rules. A partner’s API key is ours too, but only because we minted it on their behalf, they don’t hold a credential that any other AWS account could verify. An internal IAM role is owned by the organisation’s AWS footprint: it exists because someone in platform-eng created it, and rotating it is an AWS problem rather than a people problem. The bank’s JWT is owned by the bank: we trust their issuer, not an identity we hold. Each of these ownership stories wants a different validation story, and gluing the wrong pair together is how you end up writing a Lambda authoriser that reimplements something the platform already does natively.
The second is blast radius when a credential leaks. A Cognito user’s token is scoped to that user and expires in an hour; losing it leaks one person’s access for the remainder of the hour. An API key is a bearer token with no built-in expiry; if a partner checks one into Git it’s valid until someone notices and rotates. An IAM signature is not a bearer token at all, the secret never crosses the wire, only the signature, so leaked traffic is worthless to an attacker. A bank JWT is bearer-shaped like the Cognito token but its lifetime is the bank’s decision. The validation story needs to match: short-lived tokens are cheap to validate cryptographically; long-lived bearer credentials need a revocation story that cryptography alone can’t provide.
The third is cost shape on the hot path. Some authorisers are a signature check API Gateway does in native code before the request reaches anything we pay for; others invoke a Lambda for every request until a cache warms. An endpoint that does 200 requests per second with a cold-cache Lambda authoriser is a very different monthly bill than the same endpoint with a JWT authoriser. It also matters for tail latency: a native validator adds sub-millisecond; a Lambda authoriser adds a cold-start occasionally, and that occasional is the p99 on the dashboard.
The fourth is what gets passed downstream. A useful authoriser doesn’t just say yes or no, it hands the integration a context map of claims, roles, tenant IDs, so the handler doesn’t have to reparse the token. Cognito and JWT authorisers pass their claims natively. Lambda authorisers can return an arbitrary context map. IAM-auth passes the principal ARN. API keys pass… nothing beyond the key’s existence. What the handler needs to know about the caller determines whether the authoriser is enough, or whether it’s a start.
The fifth is failure mode when the identity provider is sick. Cognito outage: our Cognito-backed endpoints go dark. Bank OIDC outage: our B2B endpoint goes dark, but nothing else does. Internal IAM is a dependency on AWS itself, so an outage that takes out SigV4 validation has already taken out everything. API keys are the opposite, they have no external dependency at all, which is exactly why they’re fragile in every other way. Spreading authorisation across these mechanisms per-endpoint means one identity provider’s bad day stops one surface rather than all of them.
And finally, coupling to a specific provider. Some authoriser types are pinned to one identity provider; others accept any OIDC-compliant issuer; the freeform escape hatch will verify anything the team writes code for. If the mobile app’s identity provider might change in two years, one OIDC issuer to another, say, the portable shape is the one that takes the issuer URL as configuration rather than baking a provider into the authoriser type itself.
What we’ll filter on
Distilling that exploration into filters we can score each authoriser type against:
- Identity source. Cognito token, any OIDC JWT, SigV4 signature, API key, or something custom.
- Hot-path cost, does API Gateway validate natively, or does it invoke a Lambda per request (cached or not)?
- Coupling to a specific provider, pinned to Cognito, any OIDC, IAM only, or freeform?
- What propagates to the integration, claims, principal ARN, context map, or nothing?
- Availability on REST vs HTTP APIs, not every authoriser exists on both flavours.
The authoriser landscape
API Gateway supports five authorisation mechanisms; not all of them exist on both REST and HTTP API flavours.
-
IAM auth (
AWS_IAM). The caller signs the request with SigV4 using AWS credentials. API Gateway validates the signature; the caller’s identity is the IAM principal that signed. This is what “IAM-auth” means in API Gateway: not “any IAM user can call,” but “the caller proved they hold credentials for an IAM principal, and that principal must haveexecute-api:Invokepermission on the method’s ARN.” Works on REST and HTTP APIs. Latency is low (API Gateway validates the signature natively) and the identity is verifiable without external calls. -
Cognito user pool authoriser. The caller presents a JWT issued by a Cognito user pool; API Gateway validates it against the pool’s JWKS, extracts the
suband any claims, and passes them to the integration. Native; no Lambda invocation per request. The authoriser can require specific scopes. REST API only, on HTTP APIs the equivalent is a JWT authoriser pointed at Cognito’s issuer. Suits end-user-with-Cognito scenarios. -
JWT authoriser (HTTP API only). A general JWT validator. Configure the issuer URL (any OIDC-compliant IdP: Cognito, Auth0, Okta, Azure AD, self-hosted Keycloak) and the audience(s). API Gateway fetches the JWKS, validates tokens, checks audiences and expiry, and passes the claims to the integration. Zero Lambda invocations; built in to HTTP API. Not available on REST API.
-
Lambda authoriser (custom / request). A Lambda function API Gateway invokes for each request (subject to caching). The function receives the request details, decides yes/no, and returns a JSON response with either
{ "isAuthorized": true }(simple HTTP API flavour) or a full IAM policy document (REST API / request-parameter flavour). Two sub-types:
- Token authoriser (REST only, legacy): the authoriser receives only the value of a single named header.
- Request authoriser: receives the full request context, headers, path, query, stage variables, source IP.
Lambda authorisers are the escape hatch when the other types don’t fit: verifying a custom HMAC signature, checking a token against an internal revocation list, making a permission check against a database. TTL-based caching (AuthorizerResultTtlInSeconds) reduces repeated invocations for the same token.
-
API key + usage plan. Not strictly an authoriser; orthogonal to the above. An API key presented in an
x-api-keyheader identifies the caller, maps to a usage plan, and enforces throttle and quota. API keys don’t authenticate, possession is authorisation. Typically paired with a usage plan for billing-shaped access control rather than security. -
No authorisation (
NONE). Public endpoint. Anyone with the URL can call. Valid for truly public endpoints; dangerous as a default.
Side by side
| Authoriser | Identity source | Invokes Lambda | Available on | Returns | Per-method |
|---|---|---|---|---|---|
AWS_IAM |
SigV4 signature | ✗ | REST + HTTP | Principal | ✓ |
| Cognito user pool | Cognito JWT | ✗ | REST | Claims | ✓ |
| JWT (HTTP API) | Any OIDC JWT | ✗ | HTTP | Claims | ✓ |
| Lambda authoriser | Freeform (typically header) | ✓ (with TTL cache) | REST + HTTP | Yes/no + context, optionally IAM policy | ✓ |
| API key + usage plan | x-api-key header |
✗ | REST (primary) | N/A | Per stage |
NONE |
, | ✗ | REST + HTTP | , | ✓ |
Reading the table by caller:
- Mobile app users. Cognito user pool authoriser (REST) or JWT authoriser pointed at the Cognito issuer (HTTP). Native validation; no Lambda per request; claims propagate to integration.
- Third-party partners. API key + usage plan for throttle/quota; optionally a Lambda authoriser layered on top for revocation checks or IP allowlists.
- Internal AWS callers –
AWS_IAM. Internal Lambdas sign with their execution-role credentials; API Gateway validates SigV4;execute-api:Invokeon the method ARN decides. - Bank’s B2B. JWT authoriser pointed at the bank’s OIDC issuer. If HTTP API, it’s built in; if REST, a Lambda authoriser that validates the JWT is the equivalent.
Matching callers to authorisers
The picks in depth
Mobile app users. Cognito user pool authoriser (REST) or JWT authoriser (HTTP API). The mobile app signs in against a Cognito user pool we own and receives an ID token valid for about an hour. On a REST API we configure a Cognito user pool authoriser with the pool ARN and identitySource: "method.request.header.Authorization"; API Gateway validates against the pool’s JWKS, checks expiry, and exposes claims at $context.authorizer.claims.*. Per-method OAuth scope checks (Authorization Scopes: ["payments.read"]) let us enforce coarse permissions without touching handler code. Because validation is native, the authorisation step adds under a millisecond to the request. On an HTTP API the Cognito authoriser doesn’t exist as a distinct type, we use a JWT authoriser pointed at the user pool’s issuer URL and get the same result with different configuration syntax.
Third-party partners. API key + usage plan, optionally with a Lambda authoriser on top. The partner integration isn’t really an identity story; it’s a billing shape. The account manager hands out an API key and associates it with a usage plan that caps the partner at, say, 100 requests per second with a daily quota of a million calls. The partner sends x-api-key: <value>; API Gateway checks the key is valid, bumps the usage-plan counters, and forwards the request. Possession of the key is authorisation, which is exactly why, if the key leaks, the damage lasts until someone rotates it. When real identity verification matters (revocation lists, IP allowlists, per-tenant routing), a Lambda authoriser layered on top does the verification and the API key layer keeps doing the billing shape. Keep the Lambda’s TTL cache generous; partners don’t rotate tokens hourly.
Internal AWS callers – AWS_IAM with SigV4. Internal callers. Lambdas, ECS tasks, Step Functions, already have an IAM role with AWS credentials in their execution environment. AWS_IAM on the API Gateway method means those callers sign requests with SigV4; API Gateway validates the signature natively and checks that the signing principal holds execute-api:Invoke on the method’s ARN. The permission can be granted on the caller’s IAM role (principal-based, the usual pattern for same-account calls) or on the API’s resource policy (resource-based, for cross-account or VPC-endpoint-restricted cases). The thing the phrase “IAM-auth” obscures is that AWS isn’t checking whether the caller is an IAM user at all, it’s checking whether the caller proved possession of credentials for a named principal, and that principal has an execute-api:Invoke grant. No bearer token ever crosses the wire; the secret stays in the signer, only the signature travels. That property alone is worth preferring AWS_IAM for internal calls over almost any token-based alternative.
Bank’s B2B back-office. JWT authoriser on HTTP API. The bank issues short-lived JWTs from its own OIDC provider (bank.example.com/.well-known/openid-configuration) with audience claims pinned to our service. On an HTTP API, the JWT authoriser takes the issuer URL and the expected audience; API Gateway fetches the JWKS on first use, caches it, rotates automatically, and validates every request natively. Claims land at $event.requestContext.authorizer.jwt.claims.*, so the handler can make fine-grained decisions on the scope, actor, or tenant claim without reparsing the token. If this endpoint had to live on a REST API instead, a Lambda authoriser doing JWT verification is the equivalent, the cost is a per-request Lambda invocation (cacheable by TTL) and the operational cost of owning the JWKS rotation logic ourselves.
A worked example: mixing all four on one gateway
One API, four method groups, four authoriser types:
GET /me. Cognito user pool authoriser. Claims in the integration event; scopeprofile.readenforced on the method.POST /partners/:id/transactions. API key (required), attached usage plan with 100 rps throttle and 1,000,000/day quota. A request Lambda authoriser checks the key hasn’t been revoked and stamps atenantIdinto$context.authorizer.POST /internal/refresh–AWS_IAM. Internal execution role grantsexecute-api:Invokeon this specific method ARN; the method’s resource policy denies everything else.POST /b2b/settlements. JWT authoriser. Issuer pinned tobank.example.com; audience pinned topayments.example.com;actorclaim determines the sub-tenant.
Each method has its own authorizationType and authorizerId. No single endpoint stacks multiple authorisers from API Gateway’s menu, composition happens at the method level, and when we want two checks on one endpoint (API key + Lambda revocation, say) we use the orthogonal mechanism (API key) plus one authoriser (Lambda).
The Lambda authoriser for the partner endpoint looks like this (REST-flavour request authoriser):
exports.handler = async (event) => {
const apiKey = event.headers['x-api-key'];
const revoked = await isRevoked(apiKey);
if (revoked) return deny(event.methodArn);
const tenant = await lookupTenant(apiKey);
return {
principalId: tenant.id,
policyDocument: {
Version: '2012-10-17',
Statement: [{
Effect: 'Allow',
Action: 'execute-api:Invoke',
Resource: event.methodArn,
}],
},
context: { tenantId: tenant.id, plan: tenant.plan },
};
};
AuthorizerResultTtlInSeconds: 300 on this authoriser means a hot key triggers at most one Lambda invocation every five minutes; a partner doing 200 rps costs us one authoriser call every 60,000 requests rather than one per request.
What’s worth remembering
- The authoriser depends on the caller, not on the endpoint. Pick based on who’s calling, browser user, partner, internal AWS service, external JWT issuer, and the validator falls out.
AWS_IAMis SigV4, not “any IAM user.” The caller signs with role credentials; API Gateway validates the signature; the principal must holdexecute-api:Invoke. No bearer token ever crosses the wire.- Cognito user pool authoriser is native on REST APIs. Configure with the pool ARN; no Lambda per request; sub-millisecond validation.
- JWT authoriser is the HTTP API analogue. Any OIDC issuer, JWKS validated and rotated for us, zero Lambda invocations.
- Lambda authoriser is the escape hatch. Custom token format, revocation checks, multi-factor identity decisions. Cache results with
AuthorizerResultTtlInSecondsto amortise the per-request cost. - API key + usage plan is about billing shape, not identity. Possession = authorisation; layer a Lambda authoriser on top when real identity verification matters.
NONEis a legitimate option for truly public endpoints. Documented, monitored, paired with WAF and rate limits as other protections.- Mix authorisers per-method. One API, different endpoints, different authorisers, that’s how
GET /meandPOST /internal/*cohabit on the same gateway. - Authoriser returns propagate to the integration. Cognito claims, JWT claims, Lambda-authoriser
contextmap, IAM principal ARN, the handler sees the caller’s attributes without reparsing the token. - Validation cost matters on the hot path. Native validators (IAM, Cognito, JWT) are sub-millisecond; Lambda authorisers add a function invocation unless cached, and the cold-start is somebody’s p99.
Cognito for mobile users, API keys with usage plans for partners, IAM SigV4 for internal service-to-service, JWT authoriser for external OIDC issuers. One API can host all four. The work isn’t picking a favourite, it’s reading the caller’s identity story and picking the authoriser that matches.