token storage
there is no safe place to keep a token in the browser. the question is not where you put it, it is how much an attacker gets when they reach it.
every javascript-readable store is the same store
localStorage, sessionStorage and a plain variable differ in lifetime, not in
exposure. any script running on your origin can read all three, and that
includes the analytics tag, the chat widget and the dependency that got
compromised last night.
localStorage.getItem('token'); // any script on the page
sessionStorage.getItem('token'); // same, but dies with the tab
tokenInMemory; // same, but dies on refresh
so choosing between them is choosing how long the window stays open, not
whether it exists. localStorage is the worst of the three only because the
window never closes.
httpOnly cookies move the risk, they do not remove it
a cookie marked HttpOnly is invisible to document.cookie, so a script cannot
read it and cannot ship it to another host. that is a real win: the token stops
being stealable.
but the browser still attaches it to every request to your api. an attacker with
script execution does not need to read the token, they just call your endpoints
from your page and the cookie rides along. HttpOnly stops exfiltration, not
abuse.
Set-Cookie: refresh=...; HttpOnly; Secure; SameSite=Lax; Path=/auth
Secure keeps it off plain http. SameSite is what blocks another site from
triggering those authenticated requests. Path narrows where it is sent at all.
the shape that holds up
a short-lived access token in memory, and a long-lived refresh token in an
HttpOnly cookie.
the access token dies on refresh and expires in minutes, so stealing it buys very little. the refresh token survives, but a script cannot read it. neither half is safe on its own, and together they turn a full account takeover into a few minutes of access.
if your api is on another origin, this needs credentials: 'include' and a cors
policy that names your site explicitly. a wildcard origin will not send cookies,
which is the browser saving you from yourself.
the part people skip
none of the above matters if you have xss. a script on your page can call your api as the user no matter where the token lives, and storage strategy only changes how long that lasts.
the work that actually moves the needle is upstream: escaping output, a content security policy that blocks inline scripts, and knowing what your third party tags are allowed to do. token storage is damage control, not defense.
what not to do
- do not put a token in a url. it lands in server logs, in the referrer header and in the browser history.
- do not decode a jwt on the client and trust it. the signature is checked by the server. client side decoding is for showing a name, never for deciding what someone can do.
- do not use a long-lived access token because refresh is annoying. the expiry is the entire mitigation. removing it removes the plan.