Summary
On several applications I had found the JWT sitting in local storage, session storage or an unsecured cookie. Those techniques are open to XSS and token theft, in other words to session hijacking. This article explores three alternatives on a small proof of concept, a jQuery frontend and an Express backend that issues and verifies the token.
The first keeps the token in memory, in the local scope of an anonymous function: no other script can reach it, but a page reload loses it and the user has to sign in again. A token that expires after five minutes and is renewed every four limits the damage. The second uses a secure cookie, httpOnly, sameSite, secure and short-lived: JavaScript can no longer read it and it survives a reload, but it is not available across domains.
The third is a service worker. It runs in its own thread, so the token survives a reload; it is set only by the service worker on the login response, never sent back to the client, and injected into the fetch requests it intercepts. It is the safest and the most complex, and not every browser supports it.
There is no single right answer: it is a trade-off between security and experience, in particular surviving a reload. The repository that goes with the article implements each technique on its own branch.
Key ideas
- Local storage, session storage and an insecure cookie expose the token to any injected script: that is session theft.
- A closure puts the token out of reach of any other script, at the price of signing in again on every reload.
- An httpOnly, sameSite, secure and short-lived cookie survives a reload but does not cross domains.
- A service worker keeps the token in its own thread and injects it into fetch requests; the safest, the most complex, not yet universal.
Why I wrote this
On several applications I had noticed that the JWT token was stored in local storage, session storage or an insecure cookie, weak techniques vulnerable to XSS attacks and token theft.