PATTERN Cited by 2 sources
JWT tenant-claim extraction¶
Shape¶
Never accept tenantId from the request. Always extract it from
the validated identity token's immutable claim. The request body, URL
path, headers, and query parameters are treated as untrusted input;
the only authoritative source of tenant context is the signed JWT.
Canonical AWS realization with Cognito:
1. User authenticates to Cognito → JWT issued with:
"custom:tenantId": "tenant-a" (immutable custom attribute)
"custom:role": "admin" (mutable)
2. Every API request carries: Authorization: Bearer <JWT>
3. Service-side:
a. Validate JWT signature against Cognito JWKS
b. Extract custom:tenantId claim → request.context.tenantId
c. **Discard any tenantId field in request body / path / query**
d. All downstream data access uses the JWT-sourced tenantId
(Source: sources/2026-04-08-aws-build-a-multi-tenant-configuration-system-with-tagged-storage-patterns §C — "Critical security design: the service never accepts tenantId from request parameters. Instead, it extracts the tenant context from validated JWT tokens.")
Why this specific discipline matters¶
An attacker with a valid tenant-A JWT who manipulates the request body
to reference tenantId: "tenant-b" is the canonical multi-tenant
exfiltration scenario. Three classes of mitigation:
- Check that request
tenantId== JWTtenantIdand reject mismatches. Adequate but requires discipline at every endpoint + catches only explicit mismatches. Silent drift across a large codebase is possible. - Use request-body
tenantIdfor routing, log suspicious cases. Defense via detection only. Attackers get their first try for free. - Ignore request-body
tenantIdentirely; always use JWT's. The attack surface vanishes: manipulating the request can't change which tenant's data the service reads, because the service doesn't read the request for that information.
Option (3) is structurally safer than (1) — the class of "forgot to check this endpoint" bugs is eliminated, not defended against. It's the pattern AWS explicitly recommends for multi-tenant SaaS.
What makes this actually work¶
- Cryptographic integrity of the tenant claim. The JWT is signed by Cognito; the client cannot forge or modify claims. The tenant binding is cryptographically verifiable on every request.
custom:tenantIdis declared immutable at user-pool creation. This is Cognito-specific: custom attributes markedmutable: falsecannot be changed after user creation, even by a user-pool admin. Tenant-membership changes require re-provisioning the user (or using a mutable secondary attribute with a migration path).- JWT validation is nonnegotiable. Validate signature against
JWKS,
exp,iss,aud. Skipping any is how "JWT tenant extraction" becomes "attacker-controlled tenant extraction". - JWT claims are pinned for token lifetime. This is the tradeoff: tenant-membership changes don't take effect until the token refreshes. Short token lifetimes (or forced logout on membership change) are the mitigation.
Interaction with pre-token-generation hook¶
The pre-token hook is the
write-side partner of this pattern: it injects tenantId into
the JWT at issue time, typically by looking up a user-to-tenant
mapping in DynamoDB. JWT tenant-claim extraction is the read-side
discipline: downstream services trust only what the hook signed into
the token.
Together they form the full identity-layer tenant-isolation chain:
Cognito pool (per-tenant or shared)
→ User login
→ Pre-token Lambda hook: fetch user's tenantId, inject as custom claim
→ Cognito signs JWT
→ Every API call: Authorization: Bearer <JWT>
→ Backend: validate JWT, extract tenantId from claim, *never from body*
→ Query backend data scoped by JWT-sourced tenantId
Position in the tenant-isolation stack¶
JWT tenant-claim extraction is the identity-layer link in a layered tenant-isolation architecture (see concepts/tenant-isolation):
| Layer | Mechanism | Canonical instance |
|---|---|---|
| Identity | Cognito custom immutable attribute + JWT claim | This pattern |
| Token | Pre-token hook signs tenantId into JWT | patterns/pre-token-generation-hook |
| Authorization | Per-tenant policy store | patterns/per-tenant-policy-store |
| Request-routing | Authorizer propagates tenantId in request header | patterns/lambda-authorizer |
| Data | DynamoDB composite key TENANT#{id} / RDS tenant-context requirement |
— |
Each layer re-enforces tenant boundaries; a bug at any one doesn't leak data across tenants. JWT tenant-claim extraction is the must-have entry gate — it's what makes all downstream enforcement cryptographically grounded rather than trust-based.
When to deviate¶
- Admin operations that legitimately cross tenants (platform admin
looking at tenant A + tenant B) require an explicitly different
identity shape — either a dedicated admin role in the same pool
with
tenantId: "*"interpreted specially, or a separate admin identity source. Never special-case this by acceptingtenantIdfrom admin requests. - Multi-tenant batch jobs (nightly aggregations across tenants) need a service principal with multi-tenant access; they don't use user JWTs, they use service credentials with scoped IAM permissions.
Caveats¶
- Tenant-membership change latency. Users don't see the new tenant until their JWT refreshes (typically 1 hour). For time- sensitive changes (tenant offboarding), force a logout.
- Session-fixed
tenantId. A user who belongs to multiple tenants needs a multi-tenant identity model or per-tenant JWTs (one user pool per tenant), which complicates pool-switch UX. - Cognito custom-attribute limits. Cognito caps custom attributes per pool (25 at the time of the source post). If you need more than a handful of tenant-scoped identity attributes, consider fetching them via the pre-token hook into a bounded set of claims.
- Token size. Cognito JWTs have a size ceiling (~16KB); rich per-tenant claim payloads can bump into this. Keep the tenant claim small and look up details server-side by tenantId when needed.
Seen in¶
- sources/2026-04-08-aws-build-a-multi-tenant-configuration-system-with-tagged-storage-patterns
— canonical AWS recommendation: "the service never accepts
tenantId from request parameters";
custom:tenantIddeclared immutable at user-pool creation;CognitoJwtGuardvalidates the token against JWKS;TenantAccessGuardconfirms the user has access to the requested tenant; service layer uses the JWT-extractedtenantIdfor all downstream data operations. - sources/2026-02-05-aws-convera-verified-permissions-fine-grained-authorization — the same discipline at the entry point of a multi-layer tenant-isolation architecture; the JWT-extracted tenantId drives per-tenant-AVP-store routing, header propagation to backends, and data-layer re-verification. See concepts/tenant-isolation for the full layer chain.
Related¶
- patterns/pre-token-generation-hook — write-side partner: injects the tenantId claim the service later reads.
- patterns/lambda-authorizer — natural downstream: extracts the claim and drives authorization decisions.
- patterns/per-tenant-policy-store — uses the JWT-extracted tenantId to route to a per-tenant policy store.
- concepts/tenant-isolation — the layered-defense concept this pattern anchors.
- concepts/token-enrichment — the concept-level framing.
- systems/amazon-cognito — provides the immutable custom-attribute
- JWT-signing substrate.