← Back to all articles

What Is Really Inside a JWT (and Three Fatal Mistakes)

JWTEncodingPitfalls

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

  1. Putting sensitive data in the payload: phone numbers, ID numbers or internal permission details are effectively public; encryption requires JWE;
  2. Trusting a client-side decode: a frontend reading role: admin proves nothing — the server must verify the signature (and check iss, aud, exp);
  3. Ignoring the algorithm and expiry: skipping alg invites alg: none and confusion attacks; skipping exp keeps 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

DimensionJWTServer-side session
StateStateless — the server stores nothingThe server stores the session
RevocationHard; needs a blacklist or short expiryImmediate
Scaling outEasy, no shared storeNeeds shared storage or sticky sessions
Request costLarger token sent every timeJust 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

  1. "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.
  2. "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.
  3. "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 RS256 to none or 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, exp and nbf; 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.