JWTs are designed to be stateless — the server issues a token and doesn't track it — but "stateless" creates a specific revocation problem that most JWT implementations silently ignore: there is no standard way to invalidate a JWT before it expires, which means a stolen token remains valid until its exp claim passes
The previous articles on this site covered JWT basics, JWT security vulnerabilities, token refresh strategies, JWT vs opaque session tokens, and the stateless microservices trade-off. This article addresses JWT claims in depth — what each standard claim means, which are security-relevant vs informational, and how claims interact with the revocation problem.
The registered claims: what they actually enforce
JWTs have seven standard "registered" claims defined in RFC 7519. Each serves a specific purpose:
iss (Issuer): identifies who issued the token. Example: "iss": "https://auth.example.com". Not automatically verified — receiving code must explicitly check that iss matches an expected value. If not checked, a token from any issuer with your public key passes verification.
sub (Subject): identifies the principal the token is about — typically a user ID. "sub": "user-12345". The application decides what to do with the subject; the JWT library doesn't enforce anything about it.
aud (Audience): identifies the intended recipients of the token. If an API server has audience "api.example.com", it should reject tokens where aud doesn't include its identifier. Critically: many JWT libraries don't check aud by default — they only verify the signature. Failing to validate aud means a token issued for one service could be used at another service signed by the same key.
exp (Expiration Time): Unix timestamp after which the token must not be accepted. JWT libraries do validate exp automatically — this is the most reliably enforced claim. The security of stateless JWT systems depends heavily on keeping exp short.
nbf (Not Before): Unix timestamp before which the token must not be accepted. Rarely used but allows issuing tokens that aren't yet valid.
iat (Issued At): Unix timestamp when the token was issued. Informational; not automatically enforced. Can be used to implement a maximum token age independent of exp.
jti (JWT ID): unique identifier for the token. If stored server-side and checked on each use, enables token revocation (invalidate the jti). This is the "stateful JWT" approach — which defeats some of the statelessness argument.
What JWT libraries actually verify (and what they don't)
The dangerous assumption: many developers believe "JWT verified" means all claims are validated. In practice, JWT library default behaviour varies significantly:
Signature verification: virtually all libraries verify the signature against the provided key. This confirms the token wasn't tampered with.
exp check: most libraries verify exp automatically and reject expired tokens. Some require explicit configuration.
iss check: most libraries do NOT check iss by default. Must be explicitly configured with the expected issuer.
aud check: most libraries do NOT check aud by default. This is a significant security gap — tokens issued for one audience can be used at another service that shares the same signing key.
nbf check: inconsistent — some libraries check, some don't.
The practical risk: if a multi-service system uses the same signing key across services (common in early JWT deployments), and service A issues tokens with aud: service-a, but service B doesn't validate aud, an attacker with a valid service A token can use it at service B.
The nbf and iat anti-replay pattern
Clock skew between servers creates a specific JWT problem: token validation fails because the issuing server's clock is slightly ahead of the receiving server's clock, causing exp or nbf checks to fail.
The standard mitigation: allow a clock skew tolerance of 60 seconds in JWT validation. A token with exp: 1731677400 (3:30:00 PM) is still accepted by a server that reads the time as 3:30:59 PM (59 seconds past expiry), within the tolerance window.
The iat anti-replay pattern: use the iat claim to implement a sliding maximum age — reject tokens older than now - max_age regardless of the exp claim:
const tokenAge = now - decoded.iat;
if (tokenAge > MAX_TOKEN_AGE_SECONDS) {
throw new Error('Token too old');
}
This prevents tokens with very long exp values from being used indefinitely even if valid.
The revocation problem: three approaches
Approach 1: Short expiry + refresh tokens
Keep access token exp short (5-15 minutes). Issue long-lived refresh tokens (days/weeks) stored server-side. When access token expires, refresh token is exchanged for a new access token — and the server can refuse to refresh a revoked refresh token.
Pros: stateless access token validation; revocation at refresh token level.
Cons: up to exp duration of access token validity after revocation; complexity.
Approach 2: Token blocklist (blacklist)
On logout or revocation, add the token's jti to a blocklist stored in a fast store (Redis). On each request, check the blocklist before accepting the token.
Pros: immediate revocation. Cons: distributed state requirement; Redis lookup on every authenticated request; defeats statelessness claim.
Approach 3: Short expiry + no refresh
Keep access tokens very short (1-5 minutes); don't issue refresh tokens. Users re-authenticate frequently (or silently via session cookie).
Pros: revocation effectively immediate (at most 5 minutes). Cons: user experience friction; requires frequent re-authentication.
Inspecting JWTs without a library: what the decoder shows
A JWT is three Base64url-encoded sections separated by dots.
Decoding the header and payload (not the signature, which requires the key) reveals:
- Which algorithm was used for signing (
algin header) - All claims, including
expformatted as a Unix timestamp - Custom claims the application added (
role,permissions,tenant_id)
What the decoder does NOT show:
- Whether the signature is valid (that requires the secret key or public key)
- Whether the token has been revoked
- Whether the
issandaudclaims are correct for the specific API
The debugging value: when debugging authentication failures, the decoder immediately shows whether exp has passed, whether the aud claim matches the expected audience, and what custom claims are present — without needing access to the signing key.
How to use the JWT Decoder on sadiqbd.com
- Debug expired tokens: paste the JWT and check the
expclaim — the tool converts Unix timestamps to human-readable dates, immediately showing whether the token expired and when - Verify claims during development: check
iss,aud, and custom claims to verify the token contains expected values before debugging the application logic that reads them - Never paste production tokens containing PII: JWT payloads are not encrypted — any tool that decodes them can read their contents; use the tool with test tokens or tokens from development environments, not production tokens containing personal data
Frequently Asked Questions
If JWT payloads aren't encrypted, can my users decode their own JWT and see their claims?
Yes — any user who holds their JWT can decode and read the payload. This is by design: JWTs are signed (guaranteeing authenticity and integrity) but not encrypted. The user can see what claims are in their token; they just can't modify the claims and re-sign without the server's private key. The security implication: don't put sensitive information in JWT claims — no passwords, no credit card numbers, no PII that the user shouldn't see. Claims like user_id, role, and exp are appropriate. For sensitive data that must be associated with the session, store it server-side and reference it by sub (subject/user ID). JWE (JSON Web Encryption) is the standard for encrypted tokens if you need confidentiality.
Is the JWT Decoder free? Yes — completely free, no sign-up required.
Try the JWT Decoder free at sadiqbd.com — decode and inspect any JWT's header, payload, and claims instantly.