Where should a browser app store a JWT: localStorage or an httpOnly cookie?
An httpOnly, Secure, SameSite cookie keeps the token out of reach of JavaScript, so an XSS bug cannot steal it, but you must then handle CSRF. localStorage is simple but readable by any script on the page.
Explain it simply
Where you keep your house key matters. localStorage is like a key in a box on the porch: easy to reach for you, and also easy for anyone who gets onto the porch. If an attacker injects a script into your page (XSS), that script can read localStorage and send your token away.
An httpOnly cookie is a key held by the guard: the browser attaches it automatically to requests, but page JavaScript cannot read it.
Example
app.post("/login", async (req, res) => {
const user = await verifyCredentials(req.body);
const token = jwt.sign({ sub: user.id }, process.env.JWT_SECRET, { expiresIn: "15m" });
res.cookie("access_token", token, {
httpOnly: true, // not readable from document.cookie
secure: true, // HTTPS only
sameSite: "lax", // limits cross-site sending (CSRF)
maxAge: 15 * 60 * 1000,
});
res.sendStatus(204);
});Trade-offs:
| localStorage | httpOnly cookie | |
|---|---|---|
| Stolen by XSS | yes | no (but attacker can still make requests from the page) |
| CSRF risk | none, you attach it manually | yes, mitigate with SameSite and CSRF tokens |
| Works across domains | easy, via header | needs CORS credentials |
Whichever you pick, fix XSS first, use short expiries and HTTPS.
What to say in the interview
"For a browser app I prefer an httpOnly, Secure, SameSite cookie because scripts cannot read it. That moves the risk to CSRF, which SameSite and CSRF tokens cover. localStorage is easier but exposes the token to XSS."
Common mistakes
- Putting long-lived tokens in localStorage.
- Forgetting
secure: true, so the cookie travels over plain HTTP.