Back to Blog
SecurityJuly 6, 20264 min read

JWT Security: The Best Ways to Handle Secure Token Storage

JWT security depends on where you store tokens. Learn why localstorage vs cookies remains a critical debate and how to prevent XSS-based token theft today.

JWTWeb SecurityAuthenticationXSSCookiesOAuth2SecurityOWASP

Managing authentication in Single-Page Applications (SPAs) often leads to a single, nagging question: where do I put the JWT? If you’ve spent any time on security audits or on-call rotations, you know that placing tokens in the wrong spot turns a minor XSS vulnerability into a full account takeover.

I’ve seen too many production systems compromised because a developer thought localStorage was "good enough" for a quick MVP. It isn't. Let’s break down the reality of secure token storage and how to actually harden your frontend against theft.

The Problem with localStorage and sessionStorage

The default instinct for most frontend devs is to toss a JWT into localStorage. It’s easy to access, persists across browser restarts, and works seamlessly with fetch headers. The problem? It is entirely exposed to any JavaScript running on your page.

If you have a single third-party script—maybe an analytics tracker or a chat widget—that contains an XSS vulnerability, your entire authentication layer is compromised. That script can simply call localStorage.getItem('token') and exfiltrate your user’s session to a remote server.

When we talk about OAuth2 token security: preventing XSS-based theft in SPAs, we are essentially trying to move the token out of the reach of the DOM. localStorage is a giant, neon sign pointing to your session data for any malicious script.

Comparing Storage Options

To understand the landscape, we have to look at the differences between client-side access and browser-managed cookies.

Storage MechanismXSS AccessibleCSRF RiskPersistence
localStorageYesNoPermanent
sessionStorageYesNoTab-scoped
HttpOnly CookieNoYes (needs defense)Configurable

The Case for HttpOnly Cookies

The gold standard for JWT security is moving the token into an HttpOnly cookie. When a cookie is marked HttpOnly, the browser forbids any JavaScript from reading it. Even if an attacker successfully injects a script into your page, they cannot steal the token via document.cookie.

However, this introduces a new challenge: Cross-Site Request Forgery (CSRF). Because browsers automatically attach cookies to requests made to the domain that issued them, an attacker could potentially trick a logged-in user into performing an action on your site.

To mitigate this, you must:

  1. Use SameSite=Strict or Lax: This tells the browser to only send the cookie if the request originates from your own site.
  2. Implement CSRF tokens: For state-changing requests (POST, PUT, DELETE), require a custom header or a hidden field that the browser doesn't automatically include.

If you are just starting your architecture, it’s worth reviewing implementing secure OAuth2 authorization code flow with PKCE in SPAs to ensure the token lifecycle itself is robust before you even worry about storage.

The "Middle Ground": In-Memory Storage

If you absolutely cannot use cookies—perhaps due to complex cross-domain API requirements—the safest approach is to store the JWT in-memory.

When the user logs in, you keep the token in a JavaScript variable. It’s never written to disk. The catch? If the user refreshes the page, the variable is wiped. You’ll need a "silent refresh" pattern using a hidden iframe or a background request to get a new token from the server using a refresh token stored in an HttpOnly cookie.

This is a significant engineering investment. It requires robust JWT security: preventing improper refresh token rotation risks to ensure the refresh process itself doesn't become the weakest link in your chain.

Hardening Your Frontend Defenses

Regardless of where you store the token, your first line of defense is preventing the XSS that would allow an attacker to reach it. You should:

  • Sanitize all user input: Never trust data rendering into the DOM.
  • Implement a strict Content Security Policy (CSP): Limit which domains can execute scripts on your page.
  • Audit your dependencies: Use npm audit or tools like Snyk to catch vulnerable packages before they hit production.

If your infrastructure needs a solid foundation, I often help teams with VPS server setup, deployment & hardening to ensure the server-side headers—like Set-Cookie flags—are configured correctly from the start.

FAQ

Q: Is sessionStorage safer than localStorage? A: Not really. While it clears when the tab closes, it is still fully accessible to any JavaScript running in that tab. It offers no protection against XSS.

Q: Can I use both cookies and localStorage? A: You can, but it’s an anti-pattern. Pick one strategy. Mixing them usually leads to synchronization bugs and double the attack surface.

Q: Are there any other ways to prevent token theft? A: Yes. Look into Token Binding or OAuth2 DPoP implementation: preventing token theft and replay, which cryptographically binds the token to the client's specific environment.

Final Thoughts

There is no "perfect" solution that is both easy to implement and bulletproof. I’ve spent days debugging CSRF issues after switching to HttpOnly cookies, and I’ve spent just as long cleaning up the mess after a compromised localStorage implementation.

If I were building a new app today, I would prioritize HttpOnly cookies with SameSite=Strict and a solid CSRF protection layer. It moves the complexity to the server, where you have more control, and keeps the frontend clean of sensitive secrets. Don't try to outsmart the browser; leverage its built-in security features whenever possible.

Similar Posts