By AndyPublished
JWT Storage: localStorage vs Cookie vs Memory
The Four Storage Options
localStorage
Persistent across tabs and sessions. Accessible from JavaScript via localStorage.getItem("jwt"). Survives browser restarts. ~5 MB per origin.
sessionStorage
Like localStorage but scoped to the tab, discarded when the tab closes. Same JavaScript-accessible API.
HttpOnly Cookie
Sent automatically by the browser with every request to the matching domain. With theHttpOnly flag set, JavaScript cannot read or write it, only the browser and the server see the value. Should also carry Secure,SameSite=Strict (or Lax), and optionally__Host- prefix for additional binding.
In-Memory Variable
A plain JavaScript variable in your app's runtime memory. Lost on page reload, every fresh load starts without a token until something restores it.
The Two Threats That Drive the Choice
XSS, Cross-Site Scripting
An attacker injects JavaScript into your page (via a vulnerable input, a third-party script, a compromised dependency, or DOM-based XSS). Their script then runs with the privileges of your origin. Anything reachable by JavaScript, localStorage, sessionStorage, in-memory variables, non-HttpOnly cookies, is reachable by the attacker.
XSS is the dominant front-end threat in 2026. The bar for a working XSS exploit is low; it shows up in real-world breach reports every quarter.
CSRF, Cross-Site Request Forgery
An attacker tricks a logged-in user's browser into making an authenticated request to your site — typically via a hidden form submission from evil.com. Because the browser automatically attaches cookies for the target domain, a vanilla cookie-based session is vulnerable. CSRF only affects credentials the browser sends automatically, i.e. cookies. Authorization headers set explicitly by JavaScript don't auto-attach.
CSRF was a much bigger problem before SameSite cookies landed; withSameSite=Strict or Lax (now default in modern browsers), cross-site cookie attachment is mostly blocked. CSRF tokens are still belt-and-braces.
Direct Comparison
The vulnerability profile of each option:
Storage | XSS-readable | CSRF-vulnerable | Survives reload | Auto-sent localStorage | YES | no | YES | no sessionStorage | YES | no | tab-scoped | no non-HttpOnly cookie | YES | yes (legacy) | YES | YES HttpOnly cookie | no | yes (legacy) | YES | YES In-memory variable | YES (live) | no | NO | no
Why "Just Use localStorage" Is the Wrong Default
The case for localStorage is convenience: a SPA reads the token, attaches it to API requests viaAuthorization: Bearer ..., never has to think about CSRF (because the header isn't auto-attached), and survives reloads.
The case against: any XSS becomes immediate token theft. The attacker's injected script runs localStorage.getItem("jwt") and exfiltrates the bearer token to their own server. They now have full account access until the token expires. The attacker doesn't need persistent access, one XSS, one exfiltrate, one replay.
Why HttpOnly Cookies Aren't a Silver Bullet
A correctly-configured HttpOnly cookie (HttpOnly,Secure, SameSite=Strict) is the strongest single option:
- ·XSS cannot read the value (HttpOnly).
- ·Cross-site requests don't carry it (SameSite=Strict).
- ·HTTPS-only transport (Secure).
The remaining XSS risk: the attacker's script can still make authenticated requests from your own origin, the browser still attaches the HttpOnly cookie automatically when the script makes a same-origin request. So XSS doesn't get the token, but it CAN perform actions as the user for as long as the page is open. That's less bad than full token theft (no replay after the tab closes), but it's not zero.
Other limitations: cookies are bound to a single domain (or a Public Suffix List subset), making cross-domain SPA architectures awkward; SameSite breaks legitimate cross-origin flows like OIDC redirects (mitigated by SameSite=Lax in most cases); and CSRF tokens are still recommended for state-changing requests as defence-in-depth.
The Hybrid Pattern Most Modern Apps Use
The widely-recommended pattern in 2026:
- ·Refresh token in an HttpOnly, Secure, SameSite=Strict cookie. Long-lived (days), restricted to the auth endpoint's path (
Path=/auth) so it's only sent when refreshing. - ·Access token (the JWT) held in an in-memory JavaScript variable. Short-lived (5-15 minutes). Attached via
Authorization: Bearerheader. - ·On page reload, the SPA's bootstrap code calls the refresh endpoint. The browser auto-attaches the refresh cookie, the server issues a new access token, the SPA holds it in memory.
XSS that steals the in-memory access token gets a 5-15-minute window. XSS cannot read the refresh cookie. CSRF is blocked by SameSite + the fact that the refresh endpoint requires the cookie (which cross-site requests won't carry). The tradeoff is one extra HTTP request on page load.
Native / Mobile Storage Is Different
Outside the browser, the threat model shifts. Mobile apps use the platform keychain (iOS Keychain, Android Keystore) which provides hardware-backed encrypted storage; tokens there are immune to web-style XSS but vulnerable to a different threat surface (rooted devices, app-to-app attacks). The patterns above are browser-specific.
Defence in Depth Regardless
- ·Content Security Policy: restricts which scripts can load. Cuts XSS exploit surface dramatically.
- ·Subresource Integrity on third-party scripts you can't avoid loading.
- ·Short access-token lifetimes (5-15 min). The blast radius of any token theft is bounded by
exp. - ·Refresh-token rotation on each use, so a stolen refresh cookie can only be used once before the legitimate user's next refresh invalidates it.
- ·Server-side revocation list for refresh tokens, JWTs can't be revoked unilaterally, but refresh tokens stored on a server can.
Summary
localStorage is the worst common choice for JWT storage, XSS turns into full token theft. HttpOnly cookies are stronger but not perfect (XSS can still act-as-user without exfiltrating the token). The hybrid pattern, refresh token in HttpOnly cookie, access token in memory, is the modern default for security-conscious SPAs. The browser's storage choice isn't a substitute for the rest of the security stack (CSP, short expiries, refresh rotation, server-side revocation), but it's the one decision developers tend to make once and never revisit, so it's worth getting right.