JWT Tokens Explained: Structure, Security, and the Common Mistakes
JWT is a clean, stateless way to transport claims — and a security minefield if you treat it like an opaque session token. Learn the format, the algorithms, and the patterns that prevent the most common JWT bugs.
JWT is everywhere in modern auth — OAuth, OpenID Connect, custom APIs, mobile apps. The format is simple. The security model is subtle. Most JWT bugs come from misunderstanding the same handful of concepts: signature vs encryption, algorithm confusion, and storage location.
The format
A JWT is three Base64url-encoded segments separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphbmUgRG9lIn0. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cThe three parts:
- Header: JSON declaring the algorithm and token type. Example:
{"alg":"HS256","typ":"JWT"} - Payload: JSON containing claims (user ID, expiration, custom data). Not encrypted — readable by anyone.
- Signature: HMAC or RSA signature over the header and payload, using a secret or private key.
JWT is signed, not encrypted
Standard claims
iss— issuer (who created the token)sub— subject (typically user ID)aud— audience (intended recipient service)exp— expiration time (Unix timestamp)nbf— not before (token isn't valid until this time)iat— issued at (when the token was created)jti— JWT ID (unique identifier; used for revocation)
Custom claims can be added freely. Convention is to namespace them (e.g., https://your.app/role) to avoid collisions with future standards.
Algorithms: HS256 vs RS256
HS256 (HMAC-SHA-256)
Symmetric — same secret signs and verifies. Simple, fast. The verifier must know the same secret as the signer, so this works only when both are trusted (single service, internal microservices with shared config).
RS256 (RSA-SHA-256)
Asymmetric — private key signs, public key verifies. The signer can be a single auth service that publishes its public key; verifiers can validate without having any signing capability. Used by every major OAuth/OIDC provider.
Other algorithms: ES256 (elliptic curve), PS256 (RSA-PSS). Performance and key sizes differ; security is comparable for properly chosen parameters.
The classic security pitfalls
1. Algorithm confusion (alg=none)
The early JWT spec allowed alg: none — meaning the signature is empty and ignored. Some libraries would accept tokens with this header. An attacker could:
- Take a valid token.
- Change the header to
{"alg":"none"}. - Modify the payload (e.g., change user ID to admin's).
- Send it. Library accepts. Game over.
Modern libraries reject alg: none by default, but explicitly verify your library's config.
2. Algorithm switching attack
If your verifier loads a public key for RS256 verification, an attacker can:
- Take a valid token signed with RS256.
- Change
algtoHS256in the header. - Sign the modified token using the public RSA key as if it were an HMAC secret.
- Library trusts the algorithm field, uses the public key as HMAC secret, validates → accepts.
Mitigation: explicitly specify the expected algorithm at verification time. Don't trust the token's alg field alone.
3. Weak HS256 secrets
HS256 secrets must be at least 256 bits (32 bytes). Many implementations use short, guessable strings. Brute-forcing a 6-character secret takes seconds with modern GPUs. Generate cryptographically random secrets of sufficient length.
4. Long-lived tokens
JWTs are typically not revocable until they expire. A leaked token with a 30-day expiration is valid for 30 days regardless of what you do server-side. Best practice: short-lived access tokens (15 minutes) plus a longer-lived, server-revocable refresh token.
5. Storing JWTs in localStorage
XSS-stolen tokens become full account compromises. localStorage is JavaScript-readable, so any XSS gets the token. Better:
- HttpOnly, Secure, SameSite=Lax cookies (best for browser apps).
- Memory-only with refresh-token rotation in cookies.
- Native secure storage for mobile apps (iOS Keychain, Android Keystore).
JWT vs session cookies
The longstanding architectural debate:
- JWT advantages: stateless, no server-side session store, easy to verify in distributed services, mobile-friendly.
- JWT disadvantages: harder to revoke before expiration, larger size than session IDs, every JWT request must verify a signature.
- Session cookie advantages: instantly revocable, smaller, easier to implement correctly.
- Session cookie disadvantages: requires server-side storage, harder for cross-domain APIs.
Rule of thumb: for browser apps to your own backend, session cookies are simpler. For mobile, OAuth, federated identity, or stateless microservices, JWT is the natural choice.
The refresh token pattern
The standard secure pattern:
- User logs in, gets a short-lived access token (15 min) + long-lived refresh token (30 days).
- Access token used for API calls; expires quickly.
- When access token expires, client uses refresh token to get a new access token.
- Refresh token stored securely (HttpOnly cookie or native secure storage).
- Refresh tokens are server-tracked, allowing revocation.
- Optional: rotate refresh tokens on each use to detect theft (if the old refresh token is used after a new one was issued, signal compromise).
Common mistakes
- Putting sensitive data in the payload. Anyone can decode it. Treat payload as public.
- Trusting the alg header. Explicitly specify expected algorithms in the verifier.
- Long expiration times. 24 hours+ is too long for an access token. Use short-lived access + refresh pattern.
- Storing in localStorage. Vulnerable to XSS. Use HttpOnly cookies or memory.
- Skipping the audience check. A token issued for service A should not be accepted by service B. Always verify
aud. - Ignoring exp during verification. Using a JWT library that doesn't check expiration by default.
- No JWT ID for revocation. Long-lived tokens that can't be invalidated server-side.
Production checklist
- ✓ Specify expected algorithm explicitly during verification
- ✓ Verify
iss,aud,exp - ✓ Use 256+ bit secrets for HS256, 2048+ bit RSA for RS256
- ✓ Short-lived access tokens (15 min) with refresh tokens
- ✓ HttpOnly, Secure, SameSite cookies for browser storage
- ✓ Refresh token rotation with theft detection
- ✓ Server-side blocklist or short TTL for revocation
- ✓ Don't put PII or secrets in payload
Key Takeaways
- JWT payload is encoded, not encrypted. Anyone with the token can read it. Never put secrets there.
- Always specify expected algorithm at verification. Algorithm confusion attacks are real.
- HS256 (symmetric) for trusted same-service auth; RS256 (asymmetric) for distributed and federated systems.
- Use short-lived access tokens (15 min) with refresh token rotation, not long-lived JWTs.
- Store JWTs in HttpOnly cookies or native secure storage. localStorage is XSS-vulnerable.