JSON Web Tokens appear in nearly every modern web application. They're also consistently misimplemented. The signature is correct, the claims are right, but the algorithm selection, storage strategy, or revocation story is broken. These are the patterns that matter in production.
Algorithm selection is the first mistake. The JWT spec allows alg: 'none' — a token with no signature. Many early JWT libraries accepted this, leading to trivial authentication bypasses. Libraries have fixed this, but the risk remains if you're not explicitly allowlisting algorithms. For symmetric signing (shared secret), use HS256 or HS512. For asymmetric (separate sign/verify keys), use RS256 or ES256. Prefer RS256 or ES256 in any system where the verification key is distributed — microservices, third-party consumers — so the signing key stays on one server.
Expiry is non-negotiable. Every JWT must have an exp claim. Access tokens should expire in 15-60 minutes. Refresh tokens can live longer (7-30 days) but need rotation on use. Without expiry, a stolen token is permanent. The tradeoff with short expiry is user experience: refresh token rotation balances security and session persistence.
Storage in browsers is the most debated topic. localStorage is convenient but vulnerable to XSS — any injected script can read it. HttpOnly cookies are immune to XSS but require CSRF protection. The current consensus for web apps: store access tokens in memory (JavaScript variable, not persisted), refresh tokens in HttpOnly cookies. This limits the blast radius of XSS to the current session.
Revocation is the hard problem. A valid JWT is self-contained and stateless — the server doesn't need to check a database. This means you can't "log out" a JWT by deleting a session row. Options: short expiry (accept the 15-minute window), a token blocklist (check a Redis set on each request — adds ~1ms latency), or refresh token rotation with revocation (invalidate the refresh token on logout, forcing re-authentication at next refresh). Each approach has cost; pick based on your threat model.
Never put sensitive data in the payload. JWT payloads are base64-encoded, not encrypted. Anyone with the token can decode and read the claims. userId and email are fine; password hashes, credit card numbers, or PII beyond what's necessary are not.