Blog · Auth · Updated 2026-08-20

JWT and session security mistakes in AI-generated auth code

JWTs are not the problem — how AI-generated auth flows store, expire, and verify them usually is.

Normal, when done right

A short-lived JWT in an httpOnly, Secure, SameSite cookie, verified against your secret on every request, paired with a refresh token to renew it. This is a sound, widely used pattern.

Actually dangerous

A token with no expiry, stored in localStorage, checked for existence but never cryptographically re-verified server-side on protected routes.

Why AI-generated auth flows get this wrong

The fastest path to a working login demo is: issue a token, save it wherever the frontend can read it easily (localStorage, no expiry set), and check for its presence on protected pages. Every step of that path works in a demo. None of it is the version that survives an XSS bug, a compromised dependency, or a stolen device — because nothing about "presence" is the same as "still valid, issued by us, for this user, recently."

How to check your app

Open your browser's devtools → Application tab. If your session token shows up under Local Storage rather than only under Cookies with an HttpOnly flag, it's readable by any script on the page. Decode the token itself (JWTs are base64, not encrypted) and check its exp claim — missing or years in the future means it effectively never expires. Then check your server code: does every protected route actually call a verify function against your secret, or does some of them just check that a token exists?

The fix

// Issue: short expiry, httpOnly cookie, never in localStorage
const token = jwt.sign({ sub: userId }, process.env.JWT_SECRET, {
  expiresIn: "15m",
});
res.cookie("session", token, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
});

// Verify: on every protected request, not just once at login
function requireAuth(req: Request) {
  const token = req.cookies.session;
  if (!token) throw new Error("No session");
  return jwt.verify(token, process.env.JWT_SECRET); // throws if invalid/expired
}

Pair the short-lived access token with a longer-lived, httpOnly refresh token used only to mint new access tokens — that gives you both a small theft window and a way to revoke a session server-side by invalidating the refresh token.

Not sure how your auth actually behaves in production?

StackSecured checks how your live app issues, stores, and verifies session tokens — not just whether login works — and flags exactly which protected routes skip verification.

Run a free scan

Common questions

Is storing a JWT in localStorage really that bad?

+

It's the most common real-world way JWTs get stolen. Anything in localStorage is readable by any JavaScript running on the page — including a single XSS bug in your own code, or in a third-party script or dependency you load. An httpOnly cookie is invisible to JavaScript entirely, which removes that entire theft path even if an XSS bug exists elsewhere in the app.

What's the actual attack if a token never expires?

+

If a JWT leaks — through a logged request, a browser extension, a compromised dependency, or an XSS bug — a token with no expiry is a permanent, unrevocable credential. A short expiry paired with a refresh-token flow limits the damage window to minutes instead of indefinitely, and gives you a way to revoke access by invalidating the refresh token.

Cookies vs localStorage — is this a real tradeoff or is one just correct?

+

For session tokens specifically, httpOnly, Secure, SameSite cookies are the safer default in almost every case — they're invisible to JavaScript (blocking XSS-based theft) and the SameSite attribute gives you CSRF protection largely for free. localStorage requires you to build XSS mitigation and CSRF protection yourself, correctly, every time.

Does this apply if I use Supabase Auth or Firebase Auth instead of rolling my own JWT?

+

Managed auth providers handle token issuance and rotation correctly by default, which removes a lot of the risk. What still matters is how your own frontend stores and sends the session they hand you — if your code pulls the session token out and stores a copy in localStorage 'for convenience,' you've reintroduced the exact same exposure the provider was designed to avoid.

More reading: CORS misconfiguration in AI-generated code · Supabase RLS: the #1 vibe-coding bug · Vibe-coding security statistics 2026