Questions › Networking/Security
An SPA needs to keep the user logged in across reloads. Compare storing the token in localStorage vs an HttpOnly cookie. What's the secure design?
Reasoning about the XSS-vs-CSRF trade-off in token storage — a decision juniors get wrong by defaulting to localStorage.
This is a trade-off between two attack classes, and the popular default (localStorage) optimizes for the wrong one. LOCALSTORAGE / sessionStorage: JavaScript reads and writes it, so your SPA can attach the token as an Authorization: Bearer header on each fetch. Because it's not an ambient cookie, it's immune to CSRF (the browser never auto-sends it cross-site). BUT it's fully readable by any JavaScript running on the page — so a SINGLE XSS vulnerability means the attacker's injected script reads the token out of localStorage and exfiltrates it, and now they have the user's credentials to use from anywhere, even after the user closes the tab. XSS token theft is the dominant real-world risk, and localStorage is maximally exposed to it. HTTPONLY COOKIE: the browser stores the token and sends it automatically, but document.cookie CANNOT read it — so even with XSS, the attacker's script can't exfiltrate the token itself (they can still make requests AS the user while the page is open, but they can't steal the durable credential to reuse elsewhere). The cost: because it's an ambient cookie, you now have CSRF exposure — which you close with SameSite=Lax/Strict plus anti-CSRF tokens on state-changing endpoints. So the secure design for most apps is: keep the token in an HttpOnly + Secure + SameSite cookie, NOT localStorage. Concretely, a common robust pattern: a short-lived ACCESS token and a long-lived REFRESH token, both in HttpOnly Secure SameSite cookies; the access token authorizes API calls, and when it expires the client hits a /refresh endpoint that rotates it. This gives you: XSS can't steal the durable credential (HttpOnly), traffic is HTTPS-only (Secure), CSRF is blunted (SameSite) and further covered by CSRF tokens, and the server keeps a revocation point (invalidate the refresh token to log out everywhere). If you're forced into Bearer-header/localStorage (e.g. a pure API consumed by native + web, cross-domain constraints), then you MUST compensate: minimize XSS aggressively (strict CSP, sanitize, Trusted Types), keep access-token lifetimes very short, and never store a long-lived refresh token in JS-reachable storage. The one-liner: cookies trade CSRF (which SameSite+tokens fix well) for protection against token THEFT via XSS (which localStorage can't fix at all) — that's usually the better trade.
Designing SPA session persistence; reviewing a 'token in localStorage' choice; weighing XSS vs CSRF.
localStorage (Bearer) HttpOnly cookie
Readable by JS? YES -> XSS steals token NO -> XSS can't exfiltrate it
CSRF exposure? none (not auto-sent) yes -> fix w/ SameSite + CSRF token
Durable theft risk HIGH (reusable anywhere) LOW (bound to the browser)
Secure default: token in HttpOnly + Secure + SameSite cookie.
Robust pattern: short-lived access token + refresh token, both HttpOnly cookies;
/refresh rotates the access token; delete refresh token = log out everywhere.
The trade: cookies swap CSRF (SameSite + tokens fix it) for immunity to
XSS token THEFT (localStorage cannot fix that) — usually the better deal.