Almost every Angular tutorial puts the JWT in localStorage and moves on. It works, it is simple, and it hands the token on a plate to any script that ever runs on your domain. On a system where an identity leak was not acceptable, we had to do it differently.
Why localStorage Is Not a Vault
localStorage is readable from any JavaScript on the same domain. One XSS hole — in your code, in a dependency, in a marketing script someone added through the admin — and the attacker has the token and everything it unlocks. The same goes for sessionStorage and for a plain cookie without the httpOnly flag.
The Pattern: Token in Memory, Refresh in a Cookie
The access token lives only in memory — a field on the auth service, nowhere on disk. An interceptor attaches it to every request. For a long-lived session the server issues a refresh token as an httpOnly, Secure, SameSite cookie: the browser sends it on its own, and JavaScript can neither read nor steal it.
When the user reloads the page, memory is empty. On startup the app calls the refresh endpoint; the cookie travels by itself, the server returns a fresh access token, and the user stays signed in without noticing a thing. A short expiry on the access token limits the damage if it ever does leak.
What Changes in the App
Three things need care. First, initial load has one extra call, so the route guard must wait for its result instead of bouncing the user to the login page immediately. Second, on a 401 the interceptor attempts a refresh once and replays the original request — without ever looping. Third, logout has to invalidate the cookie on the server, because the client cannot delete it.
The Cost, and When It Pays Off
The pattern is not free: the server has to manage refresh tokens and be able to revoke them, and every browser tab refreshes its session on its own. For an internal tool with three users it may be overkill. For anything holding other people’s personal data it is not a question of whether it pays off — only of how much you are prepared to explain why you didn’t.
A token JavaScript cannot read is a token it cannot steal — everything else is hope.