If a token is saved in localStorage, anyone can misuse it — what should you do
Don't store auth tokens in localStorage — any JS on the origin (XSS, malicious extension) can read them. Use **httpOnly + Secure + SameSite=Lax/Strict cookies** so the browser sends them automatically but JS can't read. Add CSRF protection (double-submit or SameSite). Keep tokens short-lived; refresh server-side; rotate on suspicious activity.
localStorage is readable by any JS running at the origin. A single XSS payload (or malicious browser extension) can dump it and exfiltrate the token. That's why "store JWT in localStorage" is the wrong default for auth.
The right place for auth tokens
httpOnly + Secure + SameSite cookies.
Set-Cookie: session=abc; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600HttpOnly— JavaScript cannot read it (document.cookiedoesn't expose it).Secure— only sent over HTTPS.SameSite=Lax— sent on same-site navigations, not on cross-site POSTs (CSRF protection).SameSite=Strict— even safer; not sent on third-party navigations (breaks some flows).Max-Age/Expires— short-lived.
Browser automatically includes the cookie on requests to the origin — your fetch calls work without any extra header logic.
What if you must use a JS-readable token
Some SPAs need a bearer token because the auth provider gives one (Auth0, Cognito). Mitigations:
- In-memory only — never localStorage. Lost on refresh, refreshed via a separate refresh-token cookie.
- Short TTL (5–15 min). Refresh on demand.
- Refresh token in httpOnly cookie, access token in memory.
- CSP (
Content-Security-Policy) strict — blocks inline scripts and third-party JS that would exfiltrate. - Sub-resource integrity on third-party scripts.
CSRF protection
Cookies are sent automatically by the browser, including on cross-site requests if SameSite doesn't block. Add:
SameSite=Lax(default in modern browsers) — already blocks most CSRF.- Double-submit token — server sets a random value in a cookie + page reads it from a non-httpOnly cookie or a meta tag, then sends it as a header. Server validates header == cookie.
- CSRF token from server issued per session, embedded in forms.
What attackers can do with a stolen token
- Hit the API as the user.
- Read/modify data.
- Until the token expires or is revoked, your server can't tell it's not the user.
So: server-side revocation lists (or short JWTs + refresh), rate limiting, anomaly detection (geo, IP).
Defense in depth
- CSP to block injected scripts.
- Input sanitization to prevent XSS in the first place.
- HTTP-only cookies so even successful XSS can't read auth.
- Rotate session on privilege escalation.
- Audit logs for sensitive actions.
What localStorage is fine for
- User preferences (theme, last-viewed filter).
- Draft message bodies.
- Cache of public data.
- Anything that wouldn't matter if dumped.
Interview framing
"Auth tokens in localStorage are a classic mistake — any XSS or malicious extension at the origin reads them. The right primitive is an HttpOnly; Secure; SameSite cookie — JS can't read it, browser sends it automatically, and SameSite blocks most CSRF. Pair with strict CSP to harden against XSS. If you absolutely need a JS-readable bearer token, keep it in memory only with a short TTL, and stash the refresh token in an HttpOnly cookie. Add CSRF protection: double-submit token or SameSite. Localstorage is fine for non-sensitive prefs."
Follow-up questions
- •What does HttpOnly actually prevent?
- •Compare SameSite Lax vs Strict.
- •Why is CSP defense in depth?
Common mistakes
- •Storing JWTs in localStorage.
- •Reading the token in JS to attach as a bearer header — defeats HttpOnly.
- •Ignoring CSRF because 'we use JWT'.
Performance considerations
- •Cookies are sent on every request to the origin — keep them small. Use scoped paths.
Edge cases
- •Cross-subdomain cookies.
- •Service workers and cookies.
- •Cross-origin requests with credentials.
Real-world examples
- •OWASP cheatsheets, Auth0 SPA recommendations, Vercel session cookies, NextAuth defaults.