Blog · Guides

Where to store a JWT: localStorage is the wrong default

localStorage, sessionStorage, memory and httpOnly cookies compared for storing tokens in a browser: the XSS and CSRF trade-offs, the BFF pattern, and what to use for each app type.

Casdoor Team · November 1, 2026 · 3 min read

This guide is written by the team that builds Casdoor and describes the standards as published (listed under Sources), with notes on how Casdoor implements them as of November 1, 2026. Spotted a mistake? Tell us.

Every browser-based login eventually asks the same question: where do we put the token? The answer determines what an XSS bug costs you.

A JWT is a bearer credential: whoever has it can use it (RFC 6750). So the real question is not "where is it convenient", but "what can read it, and what happens when something can".

The options

Storage Readable by JavaScript Survives refresh XSS impact CSRF impact
localStorage Yes Yes Token stolen None
sessionStorage Yes Per tab Token stolen None
In-memory variable Yes (while the page runs) No Token stolen if XSS runs while valid None
httpOnly cookie No Yes Not directly stolen Needs CSRF protection
BFF + session cookie No Yes Session cookie not readable Needs CSRF protection

localStorage

The default in most tutorials, and the default in most incidents. Any JavaScript running on the origin — including a compromised npm dependency or a third-party script — can read it. There is no mitigation except not putting secrets there.

sessionStorage

Same exposure as localStorage, scoped to one tab. Slightly less persistent, not safer.

In-memory

Keeping the token in a JavaScript variable avoids persistence, so a stolen token is only useful while the tab is open. It does not survive a refresh, so the app needs a refresh mechanism — usually a refresh token in an httpOnly cookie. This is a reasonable pattern for SPAs when done carefully.

The browser sends it automatically; JavaScript cannot read it. That closes the token-theft path, but opens the CSRF path: any site can trigger a request that carries the cookie.

Mitigate with:

  • HttpOnly, Secure, and an explicit SameSite value (Lax or Strict).
  • A CSRF token (double-submit cookie or synchronized token) for state-changing requests.
  • Short lifetimes and rotation on privilege changes.

Backend-for-frontend (BFF)

The browser holds only a session cookie. The backend holds the access and refresh tokens and calls the APIs. This is the cleanest answer for SPAs that talk to third-party APIs, because the browser never sees a bearer token at all.

A decision table

Application Recommended
Server-rendered web app Session cookie (httpOnly)
SPA calling its own backend BFF with a session cookie
SPA calling APIs directly Access token in memory + refresh in httpOnly cookie
Mobile app Platform secure storage (Keychain / Keystore)
Desktop app OS keychain

Rules that apply everywhere

  1. Never store a refresh token in localStorage. It is long-lived by design; an XSS then becomes persistent access.
  2. Keep access tokens short-lived. Minutes, not days. The shorter the window, the less a leak matters.
  3. Use httpOnly + Secure + SameSite cookies. Do not leave SameSite unset and hope.
  4. Add CSRF protection when cookies are involved. httpOnly solves theft, not forgery.
  5. Avoid third-party scripts on login pages. They share the origin with your tokens.
  6. Set a Content Security Policy. It is the mitigation that limits XSS damage when one slips through.

What not to do

  • Storing the ID token and the access token and the refresh token in localStorage "because it is easy".
  • Putting tokens in URLs. They end up in history, logs and referrer headers.
  • Encrypting a token in localStorage with a key that ships in the same bundle. That is obfuscation, not protection.
  • Trusting a token just because it is a JWT. Signature and claims still need validation.

How this relates to identity providers

Where the token is stored is the client's decision; the identity provider's job is to issue short-lived access tokens, rotate refresh tokens, and support the flows that make safe storage possible — authorization code with PKCE, refresh token rotation, and back-channel logout.

Casdoor supports OIDC with PKCE, refresh tokens and RP-initiated logout, so a SPA or BFF can follow these patterns. See refresh token rotation for the server side.

Related reading: ID token vs access token, what OpenID Connect is and OAuth grant types.