Auth Token Storage
In one line
Put the session in an httpOnly, Secure, SameSite cookie — anything readable by JavaScript is one XSS away from being stolen, and "we'll just prevent XSS" is not a plan.
What it is
The trade-off is usually framed as XSS versus CSRF, and that framing is misleading. localStorage exposes the token to any script on the origin — your code, a dependency, an analytics tag, an injected payload — and exfiltration is a single line. Cookie CSRF, by contrast, is well understood and fully mitigated by SameSite plus a token. One risk has a complete defence; the other does not. That asymmetry is why httpOnly cookies win.
The flags are the whole mechanism. HttpOnly removes it from document.cookie. Secure restricts it to HTTPS. SameSite=Lax or Strict blocks cross-site attachment. Path and Domain should be as narrow as the app allows — a cookie scoped to the parent domain is readable by every subdomain, including a compromised one. __Host- as a name prefix enforces Secure, no Domain, and Path=/, which is a cheap guarantee.
Session identifiers beat self-contained tokens for browser sessions. An opaque id can be revoked instantly server-side; a JWT is valid until it expires, so logout is advisory and a stolen token stays useful. If you do use JWTs, keep them short-lived and pair them with a refresh token and a revocation list.
Refresh is where designs go wrong. The refresh token should be httpOnly, SameSite=Strict, and scoped to the refresh endpoint's path only, so it is never sent with ordinary requests. Rotate it on every use and treat reuse of a rotated token as theft: revoke the family. Never keep a refresh token where script can read it.
Two application-level rules. In-memory storage is the least-bad option when a cookie genuinely will not work — a token in a closure dies on refresh, which is inconvenient but not persistent. And regardless of storage, rotate the session id on privilege change — login, elevation, password change — or you have session fixation.
Where multiple tabs are involved, the BroadcastChannel API keeps logout consistent across them; without it, one tab stays "logged in" after another signs out, which is a real correctness and support problem.
Why it matters
"Where do you store the JWT?" is close to a guaranteed frontend interview question, and localStorage — still the most common answer — is the wrong one for a specific, explainable reason.
It is also a high-consequence decision: a stolen session is full account takeover, and the storage choice determines how easily one is stolen.
Key points
localStorageis script-readable, so any XSS or malicious dependency exfiltrates the session in one line.- CSRF has a complete defence (
SameSiteplus a token); XSS-driven token theft does not — hence httpOnly cookies. - Set
HttpOnly,Secure,SameSite, narrowPath/Domain, and use the__Host-prefix where possible. - Opaque session ids can be revoked instantly; JWTs stay valid until expiry, making logout advisory.
- Refresh tokens are httpOnly,
SameSite=Strict, path-scoped, rotated on use, with reuse treated as theft. - Use in-memory storage if a cookie is impossible — it dies on refresh rather than persisting.
- Rotate the session id on login and privilege change, and sync logout across tabs with
BroadcastChannel.