A user reports they can't see Tenant B's billing page even though they should be an admin there. The backend says "the role claim is member." The Clerk dashboard says they're an org admin. Both are right — Clerk emits org_role: "org:admin" for the active organization at sign-in, and "active organization" isn't always what you think. Generic JWT debuggers show you the claim. They don't show you that the claim's name and meaning differ between Clerk, WorkOS, Auth0, Supabase, Stack Auth, Firebase, and Cognito. This guide walks the differences, the 13 security rules every multi-tenant token should pass, and the failure vectors that don't show up at jwt.io.
Why Multi-Tenant Tokens Need Their Own Debugger
A single-tenant JWT carries identity. A multi-tenant JWT carries identity plus tenant context — which org/team/workspace this token is scoped to, what role the user has within that scope, and which permissions the role implies. The same human can be an admin in Tenant A, a viewer in Tenant B, and have no access to Tenant C, all simultaneously. The token has to pick one of these scopes for any given request.
Different providers solve this differently. The Multi-Tenant JWT Debugger surfaces the provider, decodes its specific claim conventions, and runs a 13-rule security checklist. The JWT Decoder is the right tool for generic single-tenant tokens; this is the right tool when "is this user an admin?" depends on which tenant the request is hitting.
Provider Conventions, Side by Side
| Provider | iss pattern |
Tenant claim | Role claim | Notes |
|---|---|---|---|---|
| Clerk | https://clerk.* or https://*.clerk.accounts.dev |
org_id |
org_role (org:admin, org:member) |
Roles prefixed org: to disambiguate from user-level |
| WorkOS | contains workos |
organization_id |
role (free-form string) |
App defines its own role values |
| Auth0 | https://*.auth0.com/ |
custom claim (namespaced URI) | namespaced custom claim | Roles must be namespaced under a URI like https://your-app/roles to survive the Auth0 rules pipeline |
| Supabase | ends /auth/v1 |
app_metadata.tenant_id |
app_metadata.role |
App-controlled via app_metadata; user_metadata is user-controlled and unsafe for auth |
| Stack Auth | https://api.stack-auth.com/* |
selected_team_id |
team_role |
Stack Auth uses "team" terminology; selected_team_id reflects active team |
| Firebase | https://securetoken.google.com/* |
custom claims | custom claims | Set via Admin SDK setCustomUserClaims; survive token refresh |
| Cognito | https://cognito-idp.*.amazonaws.com/* |
cognito:groups (array) |
cognito:groups |
Groups are flat; no separate role concept; ordering matters for cognito:preferred_role |
Three patterns shake out:
- Active-scope-in-token. Clerk, Stack Auth — the token reflects which org/team is currently active. Switching org issues a new token. Backend reads exactly one tenant per request.
- Custom-claim-driven. Auth0, Supabase, Firebase — the IdP doesn't model tenancy; you put it in custom claims and the backend reads them out. Maximum flexibility, maximum chance of misconfiguration.
- Group-based. Cognito — tenancy is a group membership, not a structured claim. Group naming convention is the whole spec.
If you migrate between providers, the claim names change, and so does the timing of when the token reflects scope changes. A migration from Clerk to WorkOS isn't just a vendor swap — it changes whether "active org" is a token concept or an application concept.
The 13-Rule Security Checklist
The Multi-Tenant JWT Debugger runs this on every decoded token. The rules split into three tiers by severity.
Critical (5 rules — token is unsafe to trust)
alg: none. The token claims it doesn't need a signature. Any production verifier must reject this. The 2015 round of JWT library CVEs were exactly this.- Symmetric algorithm with public issuer. HS256 with a shared secret is fine for internal service-to-service tokens. It's catastrophic for public issuers — anyone who can guess (or has previously seen) the secret can mint admin tokens.
- Token expired (
expin the past). Hard reject. No grace period. - Not yet valid (
nbfin the future). Either a clock-skew bug or a deliberately-pre-issued token. Reject and investigate. - No
expclaim. A token that never expires is a credential. If it's also stored client-side, it's a credential anyone who steals it gets indefinitely.
Warning (5 rules — works, but smells wrong)
- Tenant in
sub.subis the user identifier; puttingtenant_idinsubmeans your subject is a (user, tenant) pair. Some libraries assumesubis stable per user; this breaks them. - Wildcard permissions.
*:*,admin:*, or a permissions array longer than 20 items usually indicates a permissions model that grew faster than it was pruned. - Lifetime > 24h. Convenient, increases the exposure window if the token leaks. Default to 1h with refresh.
- Missing
aud. No audience claim means the token doesn't pin which service should accept it. A token meant for Service A can be replayed against Service B with the same issuer. - Admin/owner role in 5+ tenants. Often legitimate for support staff, often a misconfigured account that survived an employee offboarding. Worth flagging.
Info (3 rules — note for context)
- Impersonation markers (
act,on_behalf_of). Usually legitimate — service-to-service calls, support engineer impersonating a customer. Worth surfacing so audit logs can correlate. - Uncommon algorithm. Not RS256/ES256/HS256. ES512 is fine but unusual; PS256 is solid but the validator support is uneven.
- No
jti. Without a token ID, you cannot revoke individual tokens — only by rotating the signing key, which invalidates everything.
The Missing-aud Failure Vector
This one is worth a section. A token issued for Service A — but with no aud claim — can be accepted by Service B if B trusts the same issuer. Concrete scenario:
- Service A is your customer-facing API. Tokens are issued with
iss: your-idpand scopes for customer actions. - Service B is your internal admin API. It also trusts
iss: your-idpand checks the role claim. - A token issued to a customer (so it has
iss: your-idpandrole: customer) gets stolen. - The attacker presents it to Service B. B validates the issuer (matches) and the signature (matches). B reads the role (customer) and refuses.
That's the lucky case. The unlucky case is when Service B has a bug where role check happens after the token is admitted as authenticated, and the admission step doesn't check audience. Or when an internal microservice trusts any token from the IdP because "we're behind the firewall." Or when the developer who wrote Service B's auth middleware forgot the audience check entirely.
The fix is mechanical: every JWT verifier checks aud against an allowlist, full stop. The debugger flags missing aud as a Warning precisely because the failure isn't in the token — it's in whatever middleware accepts it.
Verifying Signatures in the Browser
The Multi-Tenant JWT Debugger verifies signatures three ways:
- HMAC (HS256/384/512) — paste the shared secret; verification is local and instant.
- Asymmetric (RS256, ES256, ES384, PS256) — paste a PEM public key or a JWK; verification uses SubtleCrypto in the browser. No private key needed.
- JWKS URL fetch — paste the issuer's JWKS endpoint; the tool fetches the public keys and tries each one. Lets you verify production tokens against the live key set.
Whatever you paste — token, secret, public key — stays in the browser. There's no server round-trip. (The JWKS fetch goes to the IdP's published endpoint, which is the same endpoint your backend hits during normal operation.)
Generating Test Tokens
For local backend testing, you often need a token that matches your production token's shape but is locally minted. The debugger's test-token generator takes a shape (issuer, audience, sub, role, tenant claims) and a secret/key, and produces a signed token in the chosen algorithm. Useful for:
- Writing unit tests that exercise the role-check middleware
- Reproducing a customer's reported bug locally without needing their actual token
- Smoke-testing a new tenant's permission model before users hit it
For production verification of incoming tokens, never generate them — only decode and analyze.
The Honest Limitations
- Encrypted JWE not supported. JWE tokens use authenticated encryption (typically
A256GCMoverRSA-OAEP-256). The decoder shows the outer structure but can't read the payload without the decryption key, and the security checklist doesn't apply to opaque payloads. - JWKS rotation timing not modeled. The tool fetches the JWKS at request time. If the IdP rotated keys 30 seconds ago and your token was signed by the previous key, the live fetch won't have that key — the debugger says "signature invalid" even though the token was legitimately issued.
- mTLS-bound tokens (
cnfclaim) shown but not validated. RFC 8705 specifies that some tokens are bound to the client's TLS certificate. The debugger surfaces thecnfclaim so you know binding is in place, but it doesn't have the client cert to verify the binding holds. - Provider detection is heuristic. Custom IdPs or self-hosted Keycloak deployments may not match any of the seven provider fingerprints; the tool falls back to generic JWT analysis in that case.
Use the debugger to understand a token. Use your IdP's docs to understand what claims that IdP guarantees, and your validator's docs to understand which of those guarantees your code actually checks.
Related Tools
- Multi-Tenant JWT Debugger — provider detection + 13-rule security checklist + signature verification + test-token generator
- JWT Decoder — generic single-tenant decoder for tokens that don't need provider context
- Base64 — JWT segments are base64url; decode them manually when you need to
- Hash Generator — compute the SHA-256 of a token for safe logging
- UUID Generator — generate stable IDs for test fixtures
TL;DR
Multi-tenant JWTs encode tenant scope, not just identity, and every provider names the same concept differently — Clerk's org_role is WorkOS's role is Auth0's namespaced custom claim is Cognito's cognito:groups. The 13-rule checklist catches alg: none, missing aud, wildcard permissions, lifetime > 24h, admin in 5+ tenants, and nine more. The missing-aud failure is the one that bites in production. The Multi-Tenant JWT Debugger runs all of it without sending the token off-device.