Back to blog
Logic Gap Files3 min read

JWT in localStorage: the auth mistake AI coding tools make by default

AI coding tools write working login flows. The problem is that working and secure are not the same thing. When you ask an AI to handle authentication, it almost always stores your JWT in localStorage and that single decision puts every active user session at risk.

JWT in localStorage: the auth mistake AI coding tools make by default

AI coding tools write working login flows. The problem is that working and secure are not the same thing. When you ask an AI to handle authentication, it almost always stores your JWT in localStorage and that single decision puts every active user session at risk.

What AI actually generates

When you prompt an AI tool to build a login handler, you get something like this:

// Login handler — AI generated
const { token } = await res.json();
localStorage.setItem('token', token);

This code works. Users can log in, stay logged in, and everything behaves as expected. The problem is not what it does. The problem is where it puts the token.

localStorage is readable by any JavaScript running on your page. That includes your own code, but it also includes every analytics script, chat widget, ad pixel, and third party library your app loads. If any one of those scripts is compromised, or if your app has a single XSS vulnerability, an attacker can silently read every session token stored in localStorage across your entire user base.

Why this pattern keeps appearing

AI tools reach for localStorage because it is the simplest way to persist a value across page loads. It requires no server configuration, no cookie flags, and no extra setup. The code is short, clean, and does exactly what you asked for: it stores the token.

What the AI does not account for is the threat model. localStorage was never designed to hold sensitive credentials. It has no access controls. JavaScript can read it, write it, and clear it from anywhere on the page, including code you did not write and did not audit.

Here is what the AI checks off when it generates that pattern:

RequirementStatus
Login functionality works
XSS protection
Token isolation from third party scripts
Secure cookie configuration

Three out of four requirements fail silently. Your app ships and everything looks fine until it is not.

The vulnerable pattern versus the secure one

The fix is not a different token format. It is a different location entirely.

Here is what the vulnerable client side pattern looks like:

// login.tsx — runs in the browser
const res = await fetch('/api/login', opts);
const { token } = await res.json();

// any script on the page can read this
localStorage.setItem('token', token);

And here is the secure alternative, set server side:

// /api/auth/login/route.ts — server only
const res = NextResponse.json({ ok: true });
res.cookies.set('token', jwt, {
  httpOnly: true,   // JavaScript cannot read this
  secure: true,
  sameSite: 'strict',
});
return res;

The difference is the httpOnly flag. A cookie set with httpOnly: true is completely invisible to JavaScript. Not your code, not third party scripts, not anything running in the browser. The browser reads it and sends it to your server automatically on every request. Your client side code never needs to touch it.

How to fix it in your own codebase

  1. Move token storage to the server. Your login API route should set the token as an httpOnly cookie in the response. The client should receive a success status, not the token itself.

  2. Remove any client side references to the token. If your frontend code is reading, writing, or passing around a JWT string, that is a signal the token is living somewhere JavaScript can reach it.

  3. Search your codebase for these patterns. Any of these in client side code is worth investigating: localStorage.setItem('token'), sessionStorage used for auth, JWT stored in React state, or an Authorization header being constructed in the browser.

  4. Set all three cookie flags. httpOnly prevents JavaScript access. secure ensures the cookie is only sent over HTTPS. sameSite: 'strict' protects against cross-site request forgery. All three together, not just one.

What this means for apps built with AI tools

This is not a rare edge case. It is the default output of most AI coding assistants when asked to handle authentication. If you have used Cursor, v0, Copilot, or Claude to scaffold a login flow, there is a reasonable chance your tokens are sitting in localStorage right now.

The authors who ship this pattern are not making careless mistakes. They are trusting their tools to handle the security details, and the tools are optimizing for working code rather than secure code. That gap is exactly what CodeHalo is built to catch.

If you want to see whether this pattern exists in your own repo, run a free lite scan and get a real answer in minutes.

FAQ

Does this vulnerability affect apps that use NextAuth or a third party auth library?

It depends on your configuration. NextAuth uses httpOnly cookies by default, which is the correct behavior. If you wired up your own token storage on top of a library, or customized the session handling, that custom code may have reintroduced the problem. A scan will tell you definitively.

What is XSS and why does it matter here?

XSS stands for cross site scripting. It is a class of attack where malicious JavaScript gets executed on your page, either through a flaw in your own code or through a compromised third party script. If your tokens are in localStorage, an XSS attack can read and exfiltrate them. If your tokens are in httpOnly cookies, an XSS attack cannot access them at all.

Is sessionStorage any safer than localStorage?

No. sessionStorage has the same JavaScript accessibility as localStorage. The only difference is that sessionStorage clears when the browser tab closes. Both are readable by any script on the page and neither is an appropriate place to store an auth token.

How do I verify the fix is working?

After switching to httpOnly cookies, open your browser DevTools, go to the Application tab, and look at your cookies. The token should appear there. Then open the Console and try document.cookie. Your auth token should not be visible in that output. If it is not, the httpOnly flag is working correctly.