Skip to content

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

  1. 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, or supabase.auth.token entry, your login is sitting in JavaScript-readable storage.
  2. 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.
  3. Check whether you even chose this. On Supabase and Firebase, the client SDK defaults to localStorage unless told otherwise, so you may be exposed without having written the storage code yourself.

The fix

  1. Prefer httpOnly cookies for the session. The token is set by the server in a Set-Cookie header with the HttpOnly, Secure, and SameSite=Lax flags. 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.
  2. 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.
  3. 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.
  4. Verify the change. After switching, log in again and confirm localStorage no longer holds the token, and that the session cookie shows HttpOnly in 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.