HTTP forgets everything
HTTP is stateless: every request arrives with no memory of the previous one. The server that checked Ada's password at 9:00 has no idea the 9:01 request is also Ada, unless the request carries proof.
Two strategies dominate:
- Sessions: after login, the server creates a record in a session store ("session s1 = ada") and gives the browser only the random id, usually in a cookie (a small value the browser automatically re-sends with every request). Each request looks the id up.
- Tokens: after login, the server hands back a signed token containing the data itself ("user 42, expires Friday, signature"). Each request presents the token, and the server just verifies the signature. No store, no lookup.
Same goal, opposite trade-offs, and the diagram shows the moving parts.
A complete session mechanism
login creates a store entry and returns the id, and whoAmI looks it up, with an unknown id meaning anonymous.
const sessions = new Map(); let counter = 1; function login(username) { const sessionId = "s" + counter; counter++; sessions.set(sessionId, { username: username }); return sessionId; } function whoAmI(sessionId) { const session = sessions.get(sessionId); return session ? session.username : "anonymous"; } const sid = login("ada"); console.log(sid); console.log(whoAmI(sid)); console.log(whoAmI("s999"));
Output
s1 ada anonymous
The Map is the session store, and it holds the only copy of the fact that s1 means Ada. The client never learns the username from the id, since the id is a pointer into server memory rather than a container of data.
Falling back to "anonymous" rather than throwing is what makes whoAmI usable on public routes. An unknown id and a missing id take the same path, which is correct, since neither proves anything about who is asking.
In real servers the session id is a long random string rather than s1, and predictability is the reason. A guessable id lets an attacker try s2 and land in someone else's session, so real ids come from a cryptographic random source.
The id rides in an httpOnly cookie, which is a flag meaning page JavaScript cannot read it. That way a script injected into the page cannot steal the session, which is the main defense against cross-site scripting turning into account takeover.
Note that a Map in one process is the prototype version, exactly as the in-memory repository was. Two server instances would not share sessions, which is why production session stores live in Redis or a database.
Which strategy handles "log out everywhere"
Sessions, because the server can delete the store entries and the stolen cookie ids become worthless immediately.
Sessions live server-side, so deleting them revokes access on the spot. The next request carrying that id finds nothing in the store and is treated as anonymous, with no waiting and no client cooperation.
A signed token stays valid until its expiry no matter what, because the server has nothing to delete. The token is self-contained, so the same property that removes the lookup also removes the off switch.
That is the core trade-off. Sessions cost a lookup per request and give instant revocation, and tokens skip the lookup and are hard to kill early.
| Need | Sessions | Tokens |
|---|---|---|
| revoke immediately | yes | no |
| no per-request lookup | no | yes |
| works across many servers | needs a shared store | yes |
| survives a stolen laptop | delete the session | wait for expiry |
Many real systems use short-lived tokens plus a session-like refresh mechanism to get both. The access token lasts minutes so a stolen one expires quickly, and the long-lived refresh token is stored server-side where it can be revoked.
Adding logout
logout(sessionId) removes the session and reports whether it existed, since Map's delete returns exactly that boolean.
const sessions = new Map(); let counter = 1; function login(username) { const sessionId = "s" + counter; counter++; sessions.set(sessionId, { username: username }); return sessionId; } function whoAmI(sessionId) { const session = sessions.get(sessionId); return session ? session.username : "anonymous"; } function logout(sessionId) { return sessions.delete(sessionId); } const sid = login("ada"); console.log(whoAmI(sid)); console.log(logout(sid)); console.log(whoAmI(sid));
Output
ada
true
anonymoussessions.delete(sessionId) removes the entry and returns true or false in one call, so no separate existence check is needed. That boolean is the same shape as the repository's remove from lesson 6-2, and it serves the same purpose of letting the handler pick a status code.
The three output lines are revocation demonstrated end to end. The same sid identifies Ada, then is deleted, then identifies nobody, and the client still holds the string the whole time.
That last point is the one worth internalizing. The id in the browser did not change and stopped working anyway, because authority lived in the store rather than in the token the client carried.
A real logout does one more thing, which is telling the browser to drop the cookie. Deleting the session is what makes it powerless, and clearing the cookie keeps the browser from sending a dead id on every future request.
Note that logout returning false is not an error worth reporting. Logging out twice, or logging out with an expired id, is a request whose goal is already achieved, so the handler answers 204 either way.