The Claim Nobody Registered
Decode a JWT and you get a payload full of three-letter keys:
{
"iss": "https://auth.example.com",
"sub": "user_88213",
"aud": "https://api.example.com",
"exp": 1789012345,
"iat": 1789008745,
"nbf": 1789008745,
"jti": "a3f9c2e1-4b7d-4e8a-9c1f-2d5b8e0a7f43",
"role": "editor",
"org": 4471
}
Seven of those are registered claims — defined in the JWT specification with agreed meanings. Two are yours, invented for your application.
The distinction matters more than it looks, because libraries validate the registered ones automatically and ignore the rest entirely. A claim you invented has exactly the security value of a comment unless your code explicitly checks it.
The Seven Registered Claims
iss — issuer
Who created and signed this token. A string or URI identifying the authorisation server.
Validate it. In a system with multiple identity providers, an attacker who obtains a valid token from a less-trusted issuer can present it to a service that only checks the signature. If the service accepts any signature it can verify, and it holds keys for several issuers, the token passes.
sub — subject
Who the token is about. Usually a user ID, and typically opaque and stable rather than an email address, which can change.
Should be unique within the issuer's namespace. The combination of iss and sub uniquely identifies a principal globally.
aud — audience
Who the token is for. A string or array of strings identifying the intended recipients.
This is the most frequently unvalidated claim, and one of the most important. If your API accepts a token issued for a different service — a token the user legitimately obtained for a third-party app that shares your identity provider — that app can now call your API as the user.
Every service must check that its own identifier appears in aud and reject the token if it doesn't. Many libraries only perform this check if you pass the expected audience explicitly.
exp — expiration time
Unix timestamp, in seconds, after which the token must not be accepted.
Note the unit. JavaScript's Date.now() returns milliseconds, and the resulting bug — a token that appears to expire fifty thousand years from now — is common enough to be worth checking for explicitly.
nbf — not before
Unix timestamp before which the token must not be accepted. Used for tokens issued in advance of when they become valid.
Less commonly used than exp but validated by most libraries when present.
iat — issued at
When the token was created. Useful for age policies — you might accept a token that hasn't expired but still reject it if it was issued more than a certain time ago, for example when re-authentication is required for a sensitive operation.
jti — JWT ID
A unique identifier for this specific token. Enables replay detection and selective revocation: maintain a list of revoked jti values and check incoming tokens against it.
Rarely used, because maintaining that list reintroduces the server-side state JWTs were meant to avoid.
Clock Skew
Time-based claims compare against the verifier's clock, and clocks disagree.
A token issued by a server one second ahead of yours has an iat and nbf in your future. Strict validation rejects it. The user sees an authentication failure that resolves itself a second later.
Standard practice is a leeway of 30 to 60 seconds on time-based claims. Most libraries expose this as a configuration option.
jwt.verify(token, key, {
clockTolerance: 30, // seconds
audience: 'https://api.example.com',
issuer: 'https://auth.example.com'
});
Don't set leeway too generously. A five-minute tolerance extends the usable life of every token by five minutes, including compromised ones.
Custom Claims and Namespacing
Anything not in the registered set is a private claim. Two conventions exist to avoid collisions:
Namespaced URIs, the approach several identity platforms adopt:
{
"https://example.com/roles": ["editor", "reviewer"],
"https://example.com/tenant": "acme-eu"
}
Verbose, and collision-free.
Short prefixed names, common in internal systems:
{
"acme_role": "editor",
"acme_tenant": 4471
}
Either works. What matters is avoiding names that might later become registered claims, and avoiding generic names like role or id that another system in your stack might also use with a different meaning.
Decode a token with the JWT Decoder to see exactly what claims a provider is issuing:
- Paste the token.
- Read the decoded header and payload.
- Identify which claims are registered and which are custom.
Note what decoding does and doesn't tell you. Decoding is not verification. The tool shows you the contents; it doesn't prove the token is authentic or unexpired. Anyone can decode a JWT — the payload is Base64url, not encryption.
Never paste production tokens into any browser tool. A JWT is a credential. Use test tokens.
Keep the Payload Small
Two practical constraints push toward minimal claims.
Size. JWTs typically travel in an Authorization header on every request. Web servers impose header size limits — commonly 8 KB total, sometimes less. A token stuffed with permissions, profile data and preferences can approach that, and the failure mode is an opaque 431 or 400 from the server rather than anything that points at the token.
Staleness. A claim in a token reflects state at issuance. If a user's role changes, tokens issued before the change still carry the old role until they expire. The longer the expiry, the longer the stale data circulates.
This is the core JWT tradeoff. Putting authorisation data in the token avoids a lookup per request; it also means revocation isn't immediate. Short expiry times narrow the window at the cost of more refresh traffic.
Practical guidance:
- Include a stable user identifier and a small number of coarse claims
- Don't include data that changes frequently
- Don't include anything sensitive — the payload is readable by anyone holding the token
- Look up fine-grained permissions server-side rather than embedding them
The Header Matters Too
The header is short and worth reading:
{
"alg": "RS256",
"typ": "JWT",
"kid": "2026-03-key-a"
}
alg — the signing algorithm. Never trust this value to select your verification algorithm. Configure the expected algorithm on the verifier and reject anything else. Trusting the header's alg is the basis of the algorithm-confusion class of attacks, where an attacker changes it to none or switches an RS256 token to HS256 and signs it with the public key.
kid — key identifier, telling the verifier which key to use. Essential for key rotation. Validate that the kid corresponds to a key you actually trust, rather than fetching whatever it points to.
typ — media type. Usually JWT. Checking it prevents a class of token-substitution confusion where a token intended for a different purpose is presented.
A Validation Checklist
For every token your service accepts:
- Verify the signature against a key you trust, using an algorithm you configured — not one the token specified.
- Check
expwith modest clock leeway. - Check
nbfif present. - Check
issagainst your expected issuer. - Check
audcontains your service's identifier. - Check
typif you accept multiple token types. - Validate custom claims explicitly. A
roleclaim does nothing unless your code reads it and enforces it. - Check
jtiagainst a revocation list if you maintain one.
Steps 4 and 5 are the ones most often skipped, and step 7 is the one people assume is automatic.
FAQ
What's the difference between registered and custom claims? Registered claims are defined in the JWT specification with agreed semantics and are validated by most libraries. Custom claims are application-specific and only meaningful if your code checks them.
Why does my token expire immediately?
Almost always exp set in milliseconds instead of seconds, or the opposite — a value in seconds interpreted as milliseconds.
Do I need to validate the audience claim? Yes. Without it, a token issued for a different service that shares your identity provider may be accepted by yours.
Is the JWT payload encrypted? No. It's Base64url-encoded, which is trivially reversible. Anyone with the token can read every claim. Never put secrets in it.
How long should tokens live? Short for access tokens — minutes to an hour is typical — with a refresh token mechanism for longer sessions. The right value depends on your revocation requirements.
Should I trust the alg header?
Never. Configure the expected algorithm on the verifier and reject tokens that specify anything else.
The Takeaway
A JWT's security depends on which claims get checked, and libraries check fewer than most people assume. Signature and expiry are usually automatic; issuer and audience often aren't; custom claims never are. Decoding a token to see what's actually in it is the first step toward knowing which of those checks your service is missing.
Decode and inspect JSON Web Tokens free with the JWT Decoder at sadiqbd.com — no sign-up, instant results.