Skip to content

Browser token storage reality

A practical look at JWT storage in browsers, XSS, CSRF, cookies, localStorage, service workers, and threat model tradeoffs.

Published on 2 min read

Browser token storage debates often collapse into slogans: localStorage is bad, cookies are good, or the reverse. Real applications need a threat model. A JWT in localStorage is easy for injected JavaScript to read. A JWT in an HttpOnly cookie is protected from direct script access, but it is automatically sent with requests and needs CSRF controls. Neither option saves an app that has broad XSS and privileged APIs with no step-up checks.

What an attacker wants

With XSS, the attacker first asks whether they can steal the token or simply use the victim browser as a confused deputy. If the token sits in localStorage, localStorage.getItem("access_token") is the obvious path. If it sits in an HttpOnly cookie, direct theft is harder, but the attacker can still call same-origin APIs from the page unless Content Security Policy, request validation, and sensitive action confirmations reduce the blast radius.

Token lifetime changes the calculation. A five minute access token in memory with a refresh cookie behaves very differently from a thirty day JWT in localStorage. Browser extensions, shared machines, crash dumps, logs, and support screenshots also matter. The storage choice is not isolated from operations.

Common patterns and their costs

Keeping access tokens only in memory reduces persistent theft but complicates reloads, multi-tab behavior, and offline flows. HttpOnly Secure SameSite cookies are strong for many web apps, but cross-site API domains and embedded contexts can make SameSite configuration tricky. localStorage is simple and predictable, but every XSS turns into token extraction unless the token is short lived and scoped narrowly.

Defensive details that matter

Set Secure, HttpOnly, and a deliberate SameSite value on cookies. Use CSP with nonces or hashes, not a policy that allows every analytics domain to execute script. Avoid placing tokens in URLs, because they leak through history, referrers, proxy logs, and screenshots. Do not print bearer tokens in frontend error telemetry. Scrub Authorization headers before they reach session replay tools.

Testing storage decisions

During a review, simulate the attacker, not the architecture diagram. Can injected script read a token? Can it call /api/export or /api/billing with ambient cookies? Does logout revoke the refresh path or only delete client state? Open DevTools, inspect storage, trigger errors, and check telemetry payloads. The best storage design is the one whose failure mode you have actually exercised.

Related articles

How to inspect JWT problems safely in logs, support tickets, CLI tools, screenshots, and incident channels without spreading bearer tokens.
Operational guidance for JWT signing keys: generation, storage, KMS use, rotation, access control, audit trails, and emergency response.
A field-tested sequence for testing JWT implementations, from collection and decoding to claims, keys, replay, and reporting.