JWT Claims Explained: What to Check in Every Token
A JSON Web Token is three base64url segments — header, payload, signature — and the payload is nothing but claims: assertions about a user and the token itself. Most token bugs are claim bugs: a wrong audience accepted, an expiry never checked, an issuer trusted that should not be. This article covers the claims that matter, clock skew, custom-claim restraint, and a review checklist. To see the structure yourself, paste a token into the JWT decoder.
The claims that matter: iss, sub, aud, exp
The IANA registry defines a set of registered claims, but four of them do the real security work. iss identifies who minted the token — your auth server's issuer URL. Verify it matches the expected value exactly. sub identifies who the token is about, and it must be a stable unique identifier: a database ID, not an email address that the user can change. aud identifies who the token is for; a token minted for a different service must be rejected even when the signature is valid. exp is the expiry, expressed as seconds since the Unix epoch — a token without it is a token that never dies.
| Claim | Name | What to verify |
|---|---|---|
iss | Issuer | Exactly equals your expected issuer URL |
sub | Subject | Present and a stable, unique user identifier |
aud | Audience | Contains your API's identifier; reject others |
exp | Expiration | In the future, with a small leeway for skew |
Time claims and clock skew
Three claims carry timestamps: iat (issued at), nbf (not before), and exp (expiry). All three are NumericDate values — integer seconds since the epoch, not milliseconds. Mixing up the units is a classic JavaScript bug: new Date(exp) lands in 1970, while new Date(exp * 1000) gives the real moment.
Clock skew is the second half of the story. Servers drift, so a token that expires at 12:00:00 can arrive at an API whose clock already reads 12:00:02. The standard answer is a leeway of 30 to 60 seconds applied to exp and nbf. Set leeway too high and expiry stops meaning anything; set it to zero and users get random 401s. Note also that iat is informational only — it is just another claim inside an unsigned payload.
Custom claims and token bloat
Beyond the registry you will see private claims: roles, scopes, tenant IDs, feature flags. They are useful, but three rules keep them safe. First, keep them small — the token rides in the Authorization header of every request, so a bloated payload is bandwidth and latency paid on every call. Second, never put secrets in a JWT: the payload is base64url-encoded, not encrypted, and anyone holding the token can read it. Third, avoid volatile data — permissions that change often will keep their old values until the token expires, because revocation before exp is genuinely hard. Put the stable identifiers in the token and look up the rest.
A claim-review checklist
Run this list once per token type when you build an auth flow, and again any time a token fails in a way that makes no sense. For testing, you can mint fresh tokens with the JWT generator and compute shared-secret signatures with the HMAC generator.
| Check | Reject when |
|---|---|
| Signature | Invalid, or the header says alg: none |
| Algorithm | Not the algorithm your service pins (HS256 vs RS256 mix-ups) |
iss | Does not match the expected issuer |
aud | Does not include your API identifier |
exp / nbf | Expired or not yet valid beyond the leeway |
sub | Missing, empty, or not unique per user |
None of these checks is expensive, and each closes off a real class of attack — token reuse across services, confused deputies, indefinite replay. The claims are the contract; reviewing it is cheaper than debugging the breach.
JWT Decoder — paste any JSON Web Token to inspect its header and payload claims instantly, with nothing leaving your browser.
Related tools: JWT Generator · HMAC Generator