02.07 · The token format everyone hands you

JSON Web Tokens

A compact, signed, URL-safe way to carry claims between parties. Every modern federation eventually hands you one. Every modern federation breach involves one being trusted that shouldn't have been.

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.

JWT Decoder · in-browser, no upload
The three parts highlighted: header · payload · signature
Header (algorithm + token type)
Paste a JWT above.
Payload (the claims)

        
Signature (raw bytes)

        
Verdict

The claims you actually have to know

RFC 7519 reserves seven claim names. Every JWT verifier must check at least three of them.

ClaimMeansVerifier must check?
issIssuer — the URL of the IdP that signed this token.YES — reject if not the expected issuer.
subSubject — the user this token is about. Stable per-user identifier (NOT the email).Yes — use as the user identity.
audAudience — the intended recipient. Usually the client_id.YES — reject if you are not in the audience.
expExpiration time — Unix timestamp after which the token must be rejected.YES — reject expired tokens.
nbfNot Before — the token is invalid before this time. Optional but common.If present, yes.
iatIssued At — when the token was created.Useful but not a security check by itself.
jtiJWT 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

algFamilyKeyWhen to use
HS256, HS384, HS512HMAC + 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, RS512RSA signaturesAsymmetric: private key signs, public key verifies.The federation default. Issuer keeps the private key; verifiers fetch the public key from JWKS.
ES256, ES384, ES512ECDSAAsymmetric, elliptic curve. Same trust model as RSA.Modern alternative to RS*. Smaller keys, smaller signatures.
EdDSA (Ed25519)Edwards-curve signaturesAsymmetric, 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.
The classic JWT footgun

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 alg isn't the one you expect. Do not call a verify(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.comhttps://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 recent jti values 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: none bug. 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 jwk or jku field (the key to use). Lazy verifiers fetch the key from the token, defeating the entire trust model.
  • Missing aud validation. 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 kid from the token to read a key file from disk. kid: ../../../etc/passwd caused real CVEs.
Try it — the lab

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.

Open the lab →