Copied to Clipboard
Three details matter at the API level. First, values are strings only; numbers and objects must be JSON.stringify-ed and re-parsed. Second, the scope is per-origin (protocol + host + port), which means https://app.example.com and https://api.example.com see different sessionStorage namespaces. Third, sessionStorage is also per-tab: opening the same site in a second tab gives you a fresh sessionStorage, even though localStorage is shared across tabs.
The "session" in sessionStorage refers to the browser-tab session, not the server-side authentication session. The two are unrelated and the naming is a frequent source of bugs.
How localStorage Differs (and the One Subtle Trap)?
localStorage uses the same API. The two functional differences are persistence and cross-tab scope.
localStorage.setItem('theme', 'dark');
localStorage.setItem('user.preferences', JSON.stringify({fontSize: 14}));
// Read in any tab on the same origin, including after closing and reopening
const theme = localStorage.getItem('theme');
// Cross-tab synchronization via the storage event
window.addEventListener('storage', (e) => {
console.log(`Key ${e.key} changed from ${e.oldValue} to ${e.newValue}`);
});
The subtle trap is the storage event: it fires in OTHER tabs of the same origin, not in the tab that made the change. This is the right design (you do not want to listen to your own writes) but it confuses developers who test in a single tab and conclude the event is broken.
The other localStorage gotcha is that browser-quota errors surface as a thrown exception on setItem. Defensive code catches it:
try {
localStorage.setItem('largeBlob', JSON.stringify(blob));
} catch (e) {
if (e.name === 'QuotaExceededError') {
// 5 MB limit hit; evict old entries or warn user
}
}
How Cookies Differ (and Why They Are Back in Fashion for Auth)?
Cookies predate the Web Storage API by a decade. They have weaker ergonomics for JavaScript (the API is a single string getter/setter) but two security properties the Web Storage API lacks entirely.
HttpOnly flag. A cookie set with HttpOnly cannot be read by document.cookie or any JavaScript. The browser sends it on HTTP requests automatically, but a malicious script running on the page cannot exfiltrate it. This is the property that makes HttpOnly cookies the preferred storage for sensitive auth tokens.
Secure flag. A Secure cookie is sent only over HTTPS. Combined with Strict-Transport-Security headers, this prevents the cookie from being sent over plaintext.
SameSite attribute. SameSite=Strict (sent only on same-site requests), SameSite=Lax (sent on top-level cross-site GETs), or SameSite=None (sent on all cross-site requests, requires Secure). This is the primary defense against CSRF; SameSite=Lax has been the browser default since Chrome 80 (2020).
Setting a cookie from the server in a typical Express response:
res.cookie('refresh_token', token, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/auth/refresh'
});
The browser will include this cookie on requests to /auth/refresh on the same origin, and no JavaScript on any page (including a successful XSS payload) can read its value. That is the property the Web Storage API cannot match.
Where Should I Actually Store My Auth Token?
The 2026 best-practice stack is the result of a decade of XSS post-mortems and IETF OAuth Working Group debate. The recommendation has been stable since approximately 2020.
For an OAuth-secured SPA:
Store the short-lived access token (15 minutes typical) in JavaScript memory only. A module-level variable in your auth service. Lost on page reload, which is fine because the refresh token mints a new one.
Store the refresh token (7-30 days typical) in an HttpOnly + Secure + SameSite=Lax cookie set by the server. JavaScript cannot read it, an XSS payload cannot exfiltrate it, and the browser sends it automatically on requests to the refresh endpoint.
Use CSRF tokens on state-changing requests (the cookie defense protects against most CSRF via SameSite=Lax, but a defense-in-depth CSRF token is recommended for high-value operations).
For a server-rendered application with session cookies (no SPA):
Use the framework's session cookie (Express session, Django session, Rails session). Mark it HttpOnly, Secure, SameSite=Lax.
No JavaScript token storage needed; the cookie carries the session and the server resolves it on each request.
For mobile-native apps:
- Use the platform secure storage (iOS Keychain, Android Keystore). Do not use AsyncStorage or SharedPreferences for tokens; both are readable by other apps on rooted/jailbroken devices.
Why sessionStorage and localStorage are NOT the answer for tokens. Both are readable by any script on the page. A single XSS vulnerability (a third-party script with a supply-chain compromise, a stored XSS in a comment field, a misconfigured CSP) gives the attacker full access to every token in sessionStorage and localStorage. The HttpOnly cookie pattern shifts the attack surface from "any XSS on any page" to "the specific cookie endpoint must be exploited," which is a much smaller target.
The IETF OAuth 2.0 Browser-Based Apps BCP makes this explicit. The JWT validator tool and OIDC playground are useful for inspecting tokens during the implementation work.
When Should I Use sessionStorage Then?
sessionStorage has a legitimate role; it just is not auth tokens. Three good uses:
1. Tab-scoped UI state. A multi-step form's current step, a cart state during a checkout flow, a draft that should not survive a tab close. sessionStorage is exactly the right tool: the data lives for the tab's lifetime, no longer.
2. Sensitive temporary data. Information that should not persist across sessions, like a one-time view of a recently issued recovery code or a sensitive form preview. sessionStorage clears on tab close, which is the right semantic.
3. Per-tab feature flags. A debug mode toggled for a single tab without affecting other tabs of the same site. Useful in development; rare in production.
For long-lived preferences (theme, language, accessibility settings), localStorage is the right choice. For auth tokens, HttpOnly cookies plus in-memory access tokens.
Frequently Asked Questions
What is the difference between sessionStorage and localStorage?
sessionStorage clears when the tab closes; localStorage persists until explicitly cleared. sessionStorage is per-tab; localStorage is shared across all tabs of the same origin. Both have the same API and the same ~5 MB storage limit.
Can I store a JWT in sessionStorage?
You can, but you should not for production auth. Any JavaScript on the page (including an XSS payload) can read sessionStorage values. The current best practice is to store short-lived access tokens in JavaScript memory and refresh tokens in HttpOnly + Secure + SameSite=Lax cookies.
Is sessionStorage secure?
sessionStorage is no more secure than localStorage in terms of script access. Both are readable by any JavaScript on the page. sessionStorage's only "security" benefit is the auto-clear on tab close, which is useful for temporary sensitive UI state but does not defend against XSS during the session.
What is HttpOnly and why does it matter?
HttpOnly is a cookie flag that prevents JavaScript from reading the cookie value. Set on the server with Set-Cookie: name=value; HttpOnly. It defends against XSS-based token exfiltration because even a successful XSS payload cannot read the cookie. The browser still sends it on HTTP requests automatically.
Can cookies replace localStorage?
For small (under 4 KB) per-origin data sent to the server on every request, yes. For larger client-only data, no; the 4 KB cookie size limit and the per-request transmission overhead make cookies unsuitable for general client storage.
What's the difference between sessionStorage and a server-side session?
sessionStorage is a browser-tab key-value store; it has no server component. A server-side session is server state (typically in Redis or a database) keyed by a session ID stored in a cookie. The two are unrelated and the naming overlap is unfortunate.
Final Thoughts
sessionStorage is the right tool for tab-scoped temporary UI state. It is the wrong tool for auth tokens, the same as localStorage. The 2026 best-practice stack is short-lived access tokens in JavaScript memory, refresh tokens in HttpOnly + Secure + SameSite=Lax cookies, and CSRF tokens on sensitive state-changing requests.
The shift away from localStorage for tokens has been the OWASP and IETF consensus since 2018-2020. If your codebase still stores access tokens in localStorage, scheduling the migration is the highest-ROI security work in your auth stack.
Sources
OWASP, Top 10 Web Application Security Risks 2021, owasp.org/Top10/, verified 2026年05月25日.
MDN Web Docs, Window.sessionStorage, developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage, verified 2026年05月25日.
MDN Web Docs, Window.localStorage, developer.mozilla.org/en-US/docs/Web/API/Window/localStorage, verified 2026年05月25日.
IETF, OAuth 2.0 for Browser-Based Apps BCP, datatracker.ietf.org/doc/draft-ietf-oauth-browser-based-apps/, verified 2026年05月25日.
WHATWG, HTML Living Standard: Web Storage, html.spec.whatwg.org/multipage/webstorage.html, verified 2026年05月25日.
IETF, RFC 6265: HTTP State Management Mechanism (Cookies), datatracker.ietf.org/doc/html/rfc6265, verified 2026年05月25日.