JWT claims and authorization boundaries
How to decide which claims can be trusted, which must be rechecked, and where JWTs should stop carrying authority.
JWT claims are convenient because they move facts close to the request. That convenience turns dangerous when claims start replacing authorization. A signed token can prove that an issuer made a statement at a point in time. It does not prove that the statement is still true, that the resource belongs to the same tenant, or that a user should perform a specific action right now.
Identity is not authorization
sub identifies a principal. scope or roles describe broad permission buckets. tenant_id may describe the tenant selected at login. None of those claims automatically answers, "Can this user approve this invoice?" The resource still needs ownership checks, tenant boundary checks, and sometimes state checks. Attackers look for places where a claim is treated as a database join that never happens.
A common bug appears in multi-tenant APIs: the handler trusts tenant_id from the token and uses it to filter records, but the route also accepts a tenant or account identifier in the path. If those values disagree, the system must reject the request. Silently preferring one value creates confusing behavior. Trusting the path creates an authorization bypass.
Claims that age badly
Roles, plan levels, organization membership, and feature entitlements can change while a token is still valid. If access tokens live for an hour, removing an admin role may take an hour to matter unless the API checks current state. For low risk read paths, that may be acceptable. For billing, exports, admin actions, and destructive operations, stale claims are usually not acceptable.
A practical boundary
Use JWTs for authentication facts and coarse routing decisions: issuer, subject, audience, expiration, session id, and maybe a compact scope. Use server side checks for resource authorization. When performance matters, cache authorization decisions with short TTLs and explicit invalidation rather than stuffing every permission into the token.
Testing claim abuse
During a pentest, mutate one claim at a time and watch for changes in object access. Change tenant_id, org_id, scope, role, email_verified, and feature flags. Keep the signature valid if you control a test issuer, or use low privilege tokens across endpoints to see where claims are decoded before verification. Check GraphQL especially, because resolvers sometimes apply authorization inconsistently.
Design rule
A JWT should carry enough information to identify the caller and choose the right verifier, not enough information to bypass the authorization model. When a claim affects money, data boundaries, or admin power, the API should be able to explain why it trusted that claim at that moment.