HTTP has no memory
Every request is independent. "Logged in" means only one thing: the request carried something the server recognises. There are three somethings in common use, and the choice between them is about where the state lives and how you revoke it.
| Carried as | State lives | Revoking |
|---|---|---|
| a session id in a cookie | on the server | delete the row — instant |
| a signed token (JWT) in a cookie | in the token | not until it expires, unless you keep a deny list |
Authorization: Bearer … | in the token | the same problem, plus you must store it somewhere |
Where a token may be kept
If an attacker gets script running on your page, localStorage is readable and an HttpOnly cookie is not. The cookie can still be used by that script — it rides along with a fetch — but it cannot be copied out and used elsewhere, which is the difference between one hijacked tab and a stolen account.
The usual argument for localStorage is "cookies mean CSRF". SameSite=Lax closed most of that, and a CSRF token closes the rest. The argument for tokens in a header is a genuinely cross-domain API — which is a real case, not the default one.
// an HttpOnly cookie: script cannot read it, sent automatically
Set-Cookie: sid=…; HttpOnly; Secure; SameSite=Lax; Path=/
// localStorage: any injected script reads it in one line
localStorage.setItem("token", token); // one XSS and the session is gone
An access token that lives fifteen minutes limits the damage; a refresh token in an HttpOnly, SameSite=Strict cookie scoped to the refresh path gets you a new one. That pair is the standard answer to "tokens cannot be revoked".
The two status codes, again
A client typically reacts to 401 by clearing its session and sending the user to the login page. Answering 403 with a 401 therefore logs people out for opening a page they were never allowed to see — a bug report that looks like "it keeps logging me out at random".
401 Unauthorized # I do not know who you are
WWW-Authenticate: Bearer # …and this is how to tell me
403 Forbidden # I know exactly who you are, and no
Logging out, and what it must do
- Delete the session on the server. A cookie the client throws away is not a logout.
- Clear the cookie with the same name, path and domain, and
Max-Age=0. - Rotate the session id on login as well — otherwise an id an attacker planted before you logged in is now an authenticated one (session fixation).
- On a password change, invalidate every other session.
Things that are not authentication
A hidden field, a header your own app sets, a check in the front end, an id that is hard to guess. All of them are bypassed with one curl. The server decides, on every request, from something it can verify — and CORS, as the last lesson said, is not part of that decision.