Is It Safe to Store My Login Token in localStorage?
6 min read
Your app keeps the user's login token in localStorage, and you want to know if
that is a security problem. Short answer: it is a real risk, and for a beta
app the safer default is an httpOnly cookie. Anything in localStorage can be
read by any JavaScript running on your page, so a single injected script can
steal the token and log in as your user. It is not the worst hole you can have,
but it is one worth closing before real users arrive.
Why this happens
Vibe-coded apps land on localStorage because it is the path of least
resistance. You asked the agent to "keep the user logged in," and localStorage
is the simplest thing that works: save the token after login, read it on every
request, done. It survives a refresh, it is two lines of code, and the demo
looks perfect. The agent built exactly what you asked for and nothing you didn't,
so nobody mentioned the tradeoff.
The tradeoff is this. A token in localStorage is plain text that any script on
the page can read with one line: localStorage.getItem('token'). That is fine
until a script you didn't write ends up running on your page, which is what a
cross-site scripting (XSS) bug is. If an attacker can get their JavaScript to
run (through an unescaped comment, a dodgy dependency, or user content rendered
as HTML), they read the token and impersonate that user from their own machine.
An httpOnly cookie cannot be read by JavaScript at all, so the same XSS bug can
no longer walk off with the login.
How to check
- Open your app, log in, then open your browser's dev tools. Go to
Application (Chrome) or Storage (Firefox), then Local Storage.
If you see a
token,jwt,access_token, orsupabase.auth.tokenentry, your login is sitting in JavaScript-readable storage. - Prove the risk to yourself. In the dev tools Console, on your own logged-in
session, type
localStorage.getItem('token')(use your real key name). If it prints the token, so could any script that runs on that page. - Check whether you even chose this. On Supabase and Firebase, the client SDK
defaults to
localStorageunless told otherwise, so you may be exposed without having written the storage code yourself.
The fix
- Prefer httpOnly cookies for the session. The token is set by the server
in a
Set-Cookieheader with theHttpOnly,Secure, andSameSite=Laxflags. The browser sends it automatically and JavaScript can never read it. On Next.js this means setting the cookie in a route handler or server action, not in client code. - If you use Supabase Auth, use the server-side session helpers. Their SSR
library stores the session in cookies for you instead of
localStorage. Switching to it is the intended, supported path, not a hack. - Keep tokens short-lived and refresh them. A stolen token hurts far less if it expires in an hour. See how long a login session should last for sensible defaults.
- Verify the change. After switching, log in again and confirm
localStorageno longer holds the token, and that the session cookie showsHttpOnlyin the dev tools Cookies panel.
The trap to avoid
Do not "encrypt" or obfuscate the token and leave it in localStorage, thinking
that solves it. The script that steals the token runs in the same page as your
app, so it has whatever key you used to decrypt it too. Hiding the value does
nothing; the fix is making it unreadable to JavaScript in the first place, which
is what httpOnly gives you. And do not spend a week on this while your app still
renders user content as raw HTML, because that XSS bug is the actual door.
Where this fits
Token storage is a quiet default that most vibe-coded apps get wrong without knowing, and it pairs with other login gaps like sessions that never really end. If yours does not, see why users stay logged in after logout. The free Readiness Report checks where your app keeps its session and flags a JavaScript-readable token against your real project, and the Finishing Pass hands you the switch to httpOnly cookies as an ordered fix instead of a research project.