The format, in 30 seconds
A JWT is three Base64URL-encoded JSON objects separated by dots: header.payload.signature. That's it. No magic. You can print one out and read it.
The header says which algorithm signed the token. The payload contains the claims (who, for whom, for how long, with what permissions). The signature is the cryptographic proof that the issuer wrote it and nobody changed it after.
Decode one
Paste any JWT. The tool below does everything in-browser; nothing is sent anywhere.
Paste a JWT above.
—
The claims you actually have to know
RFC 7519 reserves seven claim names. Every JWT verifier must check at least three of them.
| Claim | Means | Verifier must check? |
|---|---|---|
| iss | Issuer — the URL of the IdP that signed this token. | YES — reject if not the expected issuer. |
| sub | Subject — the user this token is about. Stable per-user identifier (NOT the email). | Yes — use as the user identity. |
| aud | Audience — the intended recipient. Usually the client_id. | YES — reject if you are not in the audience. |
| exp | Expiration time — Unix timestamp after which the token must be rejected. | YES — reject expired tokens. |
| nbf | Not Before — the token is invalid before this time. Optional but common. | If present, yes. |
| iat | Issued At — when the token was created. | Useful but not a security check by itself. |
| jti | JWT ID — unique token identifier; can be tracked for revocation/replay defense. | If you want replay defense, yes. |
Plus whatever custom claims the issuer adds: email, name, roles, scope, tenant_id, etc. None of those are reserved; treat them as untrusted until you validate the signature first.
The algorithms
| alg | Family | Key | When to use |
|---|---|---|---|
| HS256, HS384, HS512 | HMAC + SHA-2 (symmetric) | Shared secret, server-only. | Single-tenant apps where the issuer and verifier are the same system. Anyone with the secret can forge tokens. |
| RS256, RS384, RS512 | RSA signatures | Asymmetric: private key signs, public key verifies. | The federation default. Issuer keeps the private key; verifiers fetch the public key from JWKS. |
| ES256, ES384, ES512 | ECDSA | Asymmetric, elliptic curve. Same trust model as RSA. | Modern alternative to RS*. Smaller keys, smaller signatures. |
| EdDSA (Ed25519) | Edwards-curve signatures | Asymmetric, modern. | The current crypto-engineer pick. Less common in older verifiers. |
| none | "Signed with nothing" | No key. | NEVER. Disable explicitly in your verifier. The reason the JWT tampering lab exists. |
If your verifier accepts whatever alg the token specifies, an attacker can set alg: none on a forged token, omit the signature, and the verifier will accept it. Or set alg: HS256 when you were expecting RS256, and the verifier will try to validate the RSA signature using your public key as an HMAC secret. The verifier must pin the expected algorithm, not let the token choose.
The validation checklist every JWT verifier must implement
- Pin the algorithm. Reject any token whose
algisn't the one you expect. Do not call averify(token, key)function that auto-detects the algorithm from the header. - Verify the signature. With the right public key (RS*, ES*, EdDSA) or shared secret (HS*). If verification fails, reject. There is no "I'll trust it anyway" path.
- Check
iss. Must match the expected issuer URL exactly.https://accounts.google.com≠https://accounts.google.com/≠http://accounts.google.com. - Check
aud. Must include your client_id. Otherwise you might accept a token issued to a different application on the same IdP — the cross-app account hijack pattern. - Check
exp. Reject if expired. Use server time, not client time. Allow at most ~60 seconds of clock skew. - If present, check
nbf. Reject if the token's "not before" is in the future. - For replay-sensitive flows, check
jti. Track recentjtivalues and reject duplicates. - For OIDC ID tokens specifically, check
nonce. Must match the nonce you sent in step 1 of the OAuth flow.
When NOT to use a JWT
JWTs were designed to travel between systems that don't share a database. If your system does share a database, you probably want a session token, not a JWT.
- Session cookies for your own monolithic web app: use opaque session IDs backed by server-side storage. Easier to revoke, simpler, no key-management overhead.
- Long-lived bearer tokens for APIs you can change: opaque tokens you can introspect server-side beat self-contained JWTs whose only revocation strategy is "wait for them to expire."
- Anywhere you need immediate revocation: JWTs are inherently stateless. To revoke one, you either keep a server-side blocklist (defeats statelessness) or rotate the signing key (revokes everyone).
Real bugs every JWT verifier has shipped
- The
alg: nonebug. Multiple libraries accepted unsigned tokens because the verify function honored the algorithm in the token header. - The HS256/RS256 confusion bug. An attacker takes the server's RSA public key and signs a forged token with HS256 using the public key as the HMAC secret. Vulnerable verifiers compute the HMAC with the same public key and accept it.
- JWK injection. Some headers carry a
jwkorjkufield (the key to use). Lazy verifiers fetch the key from the token, defeating the entire trust model. - Missing
audvalidation. Cross-application account takeover — an attacker uses a token from app A to log into app B served by the same IdP. - kid (key ID) directory traversal. Some verifiers used
kidfrom the token to read a key file from disk.kid: ../../../etc/passwdcaused real CVEs.
JWT Tampering Lab
Decode a real JWT, change alg to none, swap role: user to role: admin, watch a deliberately weak server accept the forged token. Then turn on proper validation and watch the same payload get rejected.