The three segments
A JWT is header.payload.signature, joined by dots. The first two are Base64Url-encoded JSON — encoded, not encrypted: anyone who copies the token can decode and read it. The third segment is a signature proving the first two were not altered.
What it looks like
// header (decoded)
{ "alg": "HS256", "typ": "JWT" }
// payload (decoded) -- visible to anyone
{ "sub": "user_1024", "role": "admin", "iat": 1758000000, "exp": 1758003600 }
// the token itself
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEwMjQifQ.3f9c...(signature)
Three fatal mistakes
- Putting sensitive data in the payload: phone numbers, ID numbers or internal permission details are effectively public; encryption requires JWE;
- Trusting a client-side decode: a frontend reading
role: adminproves nothing — the server must verify the signature (and checkiss,aud,exp); - Ignoring the algorithm and expiry: skipping
alginvitesalg: noneand confusion attacks; skippingexpkeeps expired tokens valid forever.
Engineering realities
- No instant revocation: JWTs are self-contained, so revoking is hard — use short lifetimes (5–15 min) plus refresh tokens, and a blacklist when necessary;
- Size growth: stuffing permissions into the payload inflates every request header — mind the practical 4KB limit;
- Clock skew: mismatched clocks cause "expired immediately after issue"; allow a few dozen seconds of leeway.
Common questions
JWT or server sessions? Stateless, horizontally scaled services suit JWTs; when instant revocation or frequently changing permissions matter, server-side sessions are simpler. Which signing algorithm? HS256 with a shared secret for a single service; RS256/ES256 when multiple parties verify (private key signs, public key verifies).
JWT vs server-side sessions
| Dimension | JWT | Server-side session |
|---|---|---|
| State | Stateless — the server stores nothing | The server stores the session |
| Revocation | Hard; needs a blacklist or short expiry | Immediate |
| Scaling out | Easy, no shared store | Needs shared storage or sticky sessions |
| Request cost | Larger token sent every time | Just an identifier |
The common middle ground: a short-lived access token (JWT) plus a revocable refresh token held server-side — stateless scaling where you want it, instant invalidation where you need it.
Real-world cases: three security incidents
- "Setting alg to none bypassed verification": if an early library trusted the alg in the header, attackers could forge tokens. Fix: pin the accepted algorithms server-side and never trust the header.
- "The HS256 secret was the sample default": shipping the documentation secret means effectively no signature. Fix: use a long random secret, never stored in plaintext, and rotate it.
- "Tokens cannot be revoked": JWTs are stateless and stay valid until expiry. Fix: short expiry plus refresh tokens, or a server-side blocklist / version counter.
FAQ
Is a JWT encrypted? By default (JWS) no — signed only, so anyone can Base64-decode the payload; never put secrets in it. Can it carry permissions? Yes, but after a permission change old tokens still carry old claims, so rely on short expiry or a version counter. Where do I store it? localStorage is XSS-prone; HttpOnly cookies are sturdier but need CSRF care — weigh by threat model. Must I use JWT? No; server-side sessions are simpler and more controllable in many cases.
Try them: Base64 encoder/decoder, hashing
Algorithm confusion and other attack surfaces
- Algorithm confusion: if the verifier honours the algorithm named in the token header, an attacker can switch
RS256tononeor HMAC-sign with the public key — pin an allowlist of algorithms server-side; - Reject
none: no implementation should accept an unsigned token; - Validate every claim: check
iss,aud,expandnbf; verifying the signature only proves it was not altered; - Strong keys: a short HS256 secret can be brute-forced offline — use a long random one;
- No sensitive payloads: the payload is Base64-encoded, not encrypted; anyone can read it.
JWT versus sessions
JWTs give you statelessness and easy propagation across services, at the cost of instant revocation. The common compromise is a short access token (5–15 minutes) plus a long refresh token stored server-side and revocable — fewer lookups, but still able to cut someone off immediately.