JWT Storage Pitfalls
Why localStorage JWTs are XSS bait, cookie alternatives, refresh patterns, and SPA session designs that age better.
- security
- jwt
- localStorage
- xss
- auth
JWTs are a token format, not an auth architecture. The recurring frontend bug is: put a long-lived access JWT in localStorage, read it on every fetch, and assume you’re “stateless and modern.” Any XSS then exfiltrates the session in one line.
Docs: OWASP JWT, web.dev storage, XSS.
The anti-pattern
// Fragile
localStorage.setItem('access_token', jwt);
await fetch('/api', {
headers: { Authorization: `Bearer ${localStorage.getItem('access_token')}` },
});
| Property | Effect |
|---|---|
| JS-readable | XSS → full account takeover |
| Persists | Survives tab close until cleared |
| Often long-lived | Increases theft value |
Better defaults
1. HttpOnly cookie session / BFF
Browser stores opaque session id; JS never sees it. API authenticates cookie. Pair with CSRF defenses — CSRF.
2. Short-lived access token in memory + refresh cookie
let accessToken = null; // memory only
async function api(path, opts) {
const res = await fetch(path, {
...opts,
headers: { ...opts?.headers, Authorization: `Bearer ${accessToken}` },
});
if (res.status === 401) {
await refresh(); // uses HttpOnly refresh cookie
return api(path, opts);
}
return res;
}
Refresh token: HttpOnly; Secure; SameSite=Strict on a dedicated refresh endpoint. Access token dies on reload unless you re-bootstrap (call refresh on boot).
3. Avoid JWT in localStorage for session
If you must use web storage (legacy), keep TTLs tiny and accept XSS = game over — invest harder in CSP/Trusted Types.
JWT-specific footguns
- Sensitive PII in payload — base64 is not encryption; anyone with the token reads claims.
alg=none/ weak verification — server issue, know it exists.- No revocation — pure JWT without server denylist makes theft durable until expiry.
- Putting tokens in query strings — logs and Referer leaks.
XSS vs CSRF tradeoff (interview gold)
| Storage | XSS impact | CSRF impact |
|---|---|---|
| localStorage JWT | High | Low (header not auto-sent) |
| Cookie session | Lower if HttpOnly | Must mitigate CSRF |
There is no free lunch — pick the threat model.
Interview out-loud
“I don’t put long-lived JWTs in localStorage because XSS steals them. Prefer HttpOnly cookies or memory-only access tokens with HttpOnly refresh. JWT is a format; storage and TTL decisions are the real security design.”
Reviewer prompts
- What is the asset (session, PII, money movement)?
- What is the attacker capability (web, XSS, network, dependency)?
- Which control fails closed if misconfigured?
- Is the server still enforcing authz?
- Any new third party, iframe, or URL sink?
Residual risk note
Browser controls reduce likelihood and impact; they do not eliminate bugs. Prefer defense in depth: safe defaults in code, strict headers, and monitoring (CSP reports, auth anomaly alerts). When product pressure weakens a control, write down the accepted risk and a revisit date.
Related
Further depth
Teams often under-invest in this topic until an incident or CWV regression. Schedule a one-hour drill: reproduce the failure mode in DevTools, list the top three mitigations for your stack, and file tickets with owners. Revisit after the next major feature that touches networking, rendering, auth, or third parties — those are the moments regressions land. Keep primary documentation links in the runbook so on-call is not searching chat history at 2am.
Concrete artifacts to leave behind: a short architecture note, a CI assertion or header snapshot, and a dashboard panel (lab or field) that would have caught the last bug. Teaching the rest of the team the mental model matters as much as the one-line fix.
Further reading
Related guides
- Auth Session UX SecuritySession UX that doesn’t weaken security: login states, logout everywhere, idle timeouts, and step-up auth.
- Content Security Policy (CSP)Reduce XSS blast radius with CSP — script-src nonces/hashes, strict-dynamic, report-only rollout, and what CSP does not fix.
- Frontend Security Threat ModelThreat-model SPAs: assets, attackers, XSS/CSRF/supply-chain, and where frontend controls actually land.
- postMessage SecuritySecure window.postMessage: targetOrigin, event.origin checks, schema validation, and iframe bridges.
- Safe URL HandlingValidate href/src/navigation targets: block javascript: and data: traps, open redirects, and XSS via URLs.