Skip to content
Where should a browser app keep its tokens? Not localStorage (mostly)
article3 min read547 viewsAug 22, 2026

Where should a browser app keep its tokens? Not localStorage (mostly)

XSS, CSRF, HttpOnly cookies and the backend-for-frontend pattern, without the religious war.

Fatima Al-Sayed@fatima_appsec

August 22, 2026 · 3 min read

#1 in Security Theatre

547 views8 comments

This question comes up in every code review I do for a new web app, so I am writing the long answer once.

The short version

For a first-party web app (your frontend talks to your own API): keep the session in an HttpOnly, Secure, SameSite cookie, and do not put long-lived tokens anywhere JavaScript can read them. If you must use OAuth access tokens in the browser, keep the access token in memory and short-lived, and the refresh token in an HttpOnly cookie. Or better, keep both on the server with a backend-for-frontend.

Why not localStorage

Anything in localStorage or sessionStorage is readable by any JavaScript running on your origin. One XSS bug, in your code or in one of the 900 packages in your node_modules, and the attacker reads the token and sends it to their server. Now they can use it from anywhere for as long as it is valid. If it is a refresh token, that can be weeks.

People answer: "if you have XSS you already lost". Partly true, and I will come back to it. But there is a difference between an attacker who can act inside the victim's open tab for a few minutes and an attacker who has a token in their own database to use for 30 days.

Why HttpOnly cookies

An HttpOnly cookie is sent automatically by the browser and cannot be read by JavaScript. XSS can still make requests from the page while the user has it open, but it cannot exfiltrate the credential. Add:

  • Secure, so it only goes over HTTPS
  • SameSite=Lax (or Strict), which blocks the cookie on most cross-site requests and removes most of the classic CSRF risk
  • the __Host- prefix on the name, which forces Secure, Path=/ and no Domain attribute, so a subdomain cannot overwrite it

Cookies bring CSRF back into the picture, so for state changing requests keep a CSRF token or at least verify the Origin header. SameSite=Lax does not protect GET requests that change state, so do not have those.

The backend-for-frontend pattern

If you integrate with an OAuth provider, the cleanest setup is a small server component (the BFF) that runs the OAuth flow, keeps the access and refresh tokens on the server, and gives the browser only a session cookie. The browser never sees an OAuth token at all. Most frameworks people use today already have a server, so this is less work than it sounds.

About "XSS means game over"

Yes, XSS is very bad whatever you do. So also do the things that make XSS less likely and less useful:

  • A Content Security Policy without unsafe-inline for scripts
  • Framework escaping, and a review of every place you bypass it (dangerouslySetInnerHTML, v-html, innerHTML)
  • Short-lived access tokens, so a stolen one expires quickly
  • Refresh token rotation with reuse detection: if an old refresh token is used again, revoke the whole family

Storage choice is one layer. It is not the only one, but it is cheap to get right at the start and expensive to change later.

Happy to look at specific setups in the comments, but please do not paste real tokens. Yes, it happened.

Written by

Fatima Al-Sayed

Application security engineer at a bank in Dubai. Threat modelling, auth flows, secure code review. I will ask where your JWT secret lives. Arabic coffee enthusiast.

Get the best of #security-theatre weekly

One email each Monday with the most useful posts. No spam; unsubscribe in one click.

Comments

8

Sign in to join the conversation.

Sign in to comment
Tunde Adebayo@tunde_ade#4 in Security Theatre
ok so localStorage bad, but what of react native? asyncstorage is also plain text abi
0 net votes
Fatima Al-Sayed@fatima_appsec#1 in Security Theatre
Correct, AsyncStorage is not encrypted. On mobile use the platform keystore: Keychain on iOS, Keystore-backed storage on Android. Libraries like react-native-keychain or expo-secure-store wrap both.
0 net votes
Chloe Barnes@chloebuilds#4 in Security Theatre
I have tokens in localStorage on my app. I feel personally attacked. bookmarking to fix this weekend (I will not fix this weekend)
3 net votes
Fatima Al-Sayed@fatima_appsec#1 in Security Theatre
Noted. I will ask you on Monday.
1 net votes
Tomasz Kowalski@tomasz_k#3 in Security Theatre
Cookies have CSRF, localStorage has XSS. If you have XSS you lost anyway, attacker can call API from the page with the cookie. HttpOnly only stops exfiltration. People make religion from this.
1 net votes
Fatima Al-Sayed@fatima_appsec#1 in Security Theatre
Both things are true. XSS is game over for that session while the tab is open. HttpOnly decides whether it is also game over for the next 30 days from the attacker's laptop. The difference is time and scope, and in incident response time and scope are everything.
3 net votes
Tomasz Kowalski@tomasz_k#3 in Security Theatre
ok, fair point. Offline replay is real problem.
0 net votes
Kevin Tran@kevtran_dev#6 in Security Theatre
saving this, our bootcamp final project literally did localStorage.setItem("token") lol. the refresh token rotation part went over my head a bit but i'll reread it
1 net votes