{"course":{"slug":"cloud-and-shipping","title":"Shipping It","url":"https://addaly.com/learn/cloud-and-shipping","level":"intermediate","track":"build"},"lesson":{"slug":"sessions-and-logging-people-out","title":"Sessions, and taking one back","url":"https://addaly.com/learn/cloud-and-shipping/sessions-and-logging-people-out","position":59,"of":74,"minutes":9,"answer":"Design sessions around revocation (an opaque ID looked up server-side revokes instantly, while a stateless token cannot until it expires) and rotate the ID on every privilege change, hash it at rest.","keyIdea":"Design sessions around revocation (an opaque ID looked up server-side revokes instantly, while a stateless token cannot until it expires) and rotate the ID on every privilege change, hash it at rest, and re-authenticate in front of dangerous actions."},"body":{"format":"markdown","text":"## The test of a session system is revocation\n\nAnybody can issue a session. The question that separates designs is what happens when you need to end one immediately: a stolen laptop, a shared password, a member removed from a team, a support agent who has left, an account you have just banned.\n\nIf your answer is \"they stay logged in until it expires\", you do not have a session system, you have a countdown.\n\n## Two designs, and the trade nobody explains clearly\n\n**Server-side sessions.** The cookie holds a random opaque ID; the server looks it up in Redis or a table. Revoking is a delete. Reading costs one fast lookup per request. This is the boring design and it is the right default for almost every application, including large ones.\n\n**Stateless tokens (JWTs).** The token itself carries the claims, signed. No lookup, which is the entire selling point, and therefore no way to revoke, because nothing is consulted. The usual patch is short-lived access tokens (5 to 15 minutes) plus a long-lived refresh token that *is* stored server-side, so revocation takes effect at the next refresh. That is a reasonable design, and notice what it has done: reintroduced the database lookup you were avoiding, and added a window during which a revoked user is still working.\n\nChoose stateless tokens when you have several services that must verify without a shared store. Choose server-side sessions otherwise. \"JWTs scale better\" is a claim about an architecture most small products do not have, and the cost is paid in the incident where you cannot log somebody out.\n\n> Figure: Server-side session versus stateless token. Opaque ID, looked up server-side: Cookie holds a random ID; server checks Redis or a table; Revoking is a delete, effective on the next request; One fast lookup per request; The right default for almost every application. Signed token carrying its claims (JWT): No lookup, which is the whole selling point; Nothing is consulted, so nothing can revoke it until expiry; Usual patch: 5 to 15 minute access tokens plus a stored refresh token; Worth it when several services must verify without a shared store. The test of a session design is revocation. The stateless token's patch, short access tokens plus a stored refresh token, reintroduces the lookup it was avoiding and adds a window in which a revoked user keeps working.\n\n## The cookie attributes, and what each one prevents\n\n```\nSet-Cookie: sid=<random>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600\n```\n\n- **HttpOnly** keeps JavaScript from reading it, so one cross-site scripting bug does not hand over every session.\n- **Secure** keeps it off plain HTTP.\n- **SameSite=Lax** stops the cookie being sent on most cross-site requests, which removes the majority of cross-site request forgery. `Strict` breaks inbound links from other sites; `None` requires `Secure` and re-opens the risk, so use it only for a genuine cross-site flow.\n- **A random, long ID**: at least 128 bits from a cryptographic generator. Not a counter, not a hash of the user ID, not anything derived from time.\n\nStore a hash of the session ID rather than the ID itself, so a leaked database is not a set of working sessions. This is the same argument as hashing passwords, applied to the thing that substitutes for one.\n\n## Rotate on privilege change\n\nIssue a new session ID at login, and again whenever authority changes: password change, email change, second factor enabled, role granted. Two reasons: it defeats session fixation, where an attacker plants a known ID before you log in, and it ensures a password change actually ends the attacker's session rather than leaving it alongside yours.\n\nA password reset should, by default, end every other session. If you offer \"log out everywhere\", make sure it reaches API tokens and mobile apps too, or it is a comforting button that does nothing.\n\n## Show people their sessions\n\nA list of active sessions (device, approximate location, last seen, with a revoke button) is perhaps two hundred lines of work and it does three things at once: it lets a user end a session you know nothing about, it makes a compromise visible to the only person who can recognise it, and it gives your support process something to point at. Every large service has one because they are the first thing a worried user asks for.\n\n## Expiry\n\nThere is a real tension. Short sessions are safer and annoy people into choosing worse passwords and staying signed in on shared machines. Long sessions are kind and leave more time for a stolen cookie to be useful.\n\nA defensible middle: a sliding two-week session for ordinary use, an absolute maximum of a few months regardless of activity, and a re-authentication prompt in front of dangerous actions (changing email, deleting the account, viewing payment details) regardless of how fresh the session is. That last one is what protects the user whose unlocked laptop somebody borrowed for a minute.\n\n## The failure to plan for\n\nCredential stuffing is the common attack and it is not a breach of your system: somebody takes a list of passwords leaked elsewhere and tries them against your login. The defences are rate limits per account rather than only per IP, a check against known-breached password lists (the free range-query API from Have I Been Pwned does this without sending you the password), and a second factor for accounts that can do damage.\n\nWhen it succeeds, everything above is what you use: revoke the sessions, force a reset, show the user their session list, and tell them plainly what happened."},"check":{"question":"A service issues 24-hour JWTs with no server-side store. A user's laptop is stolen and they ask to be logged out everywhere. What can the service actually do?","options":[{"text":"Delete the session record, ending access immediately","correct":false,"tempting":"There is no session record; the whole design avoids one, which is exactly why this option is unavailable."},{"text":"Rotate the signing key, invalidating that token and every other token in circulation","correct":false,"tempting":"It does work, and it logs out every user of the service at once: a blunt instrument that most teams find unacceptable, though it is sometimes the only lever available."},{"text":"Nothing that takes effect before the token expires, unless a deny-list or key rotation is introduced","correct":true},{"text":"Change the user's password, which invalidates previously issued tokens","correct":false,"tempting":"Only if the token verification consults something that changes with the password, which a purely stateless design does not do."}]},"prev":{"slug":"who-is-allowed-to-do-what","title":"Authentication is not authorisation","url":"https://addaly.com/learn/cloud-and-shipping/who-is-allowed-to-do-what"},"next":{"slug":"dependencies-and-the-supply-chain","title":"The code you did not write","url":"https://addaly.com/learn/cloud-and-shipping/dependencies-and-the-supply-chain"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'Sessions, and taking one back', in Shipping It. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/cloud-and-shipping/sessions-and-logging-people-out (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"Sessions, and taking one back","courseTitle":"Shipping It","module":{"index":7,"slug":"security-after-the-short-list","title":"Security after the short list"},"answer":null,"keyIdea":"Design sessions around revocation (an opaque ID looked up server-side revokes instantly, while a stateless token cannot until it expires) and rotate the ID on every privilege change, hash it at rest, and re-authenticate in front of dangerous actions.","hero":{"kind":"figure","figure":{"t":"figure","label":"Figure","figure":{"left":{"title":"Opaque ID, looked up server-side","items":["Cookie holds a random ID; server checks Redis or a table","Revoking is a delete, effective on the next request","One fast lookup per request","The right default for almost every application"]},"type":"compare","right":{"title":"Signed token carrying its claims (JWT)","items":["No lookup, which is the whole selling point","Nothing is consulted, so nothing can revoke it until expiry","Usual patch: 5 to 15 minute access tokens plus a stored refresh token","Worth it when several services must verify without a shared store"]},"title":"Server-side session versus stateless token","caption":"The test of a session design is revocation. The stateless token’s patch, short access tokens plus a stored refresh token, reintroduces the lookup it was avoiding and adds a window in which a revoked user keeps working."},"alt":"Figure: Server-side session versus stateless token. Opaque ID, looked up server-side: Cookie holds a random ID; server checks Redis or a table; Revoking is a delete, effective on the next request; One fast lookup per request; The right default for almost every application. Signed token carrying its claims (JWT): No lookup, which is the whole selling point; Nothing is consulted, so nothing can revoke it until expiry; Usual patch: 5 to 15 minute access tokens plus a stored refresh token; Worth it when several services must verify without a shared store. The test of a session design is revocation. The stateless token’s patch, short access tokens plus a stored refresh token, reintroduces the lookup it was avoiding and adds a window in which a revoked user keeps working."}},"blocks":[{"t":"h2","id":"the-test-of-a-session-system-is-revocation","runs":[{"text":"The test of a session system is revocation"}]},{"t":"p","runs":[{"text":"Anybody can issue a session. The question that separates designs is what happens when you need to end one immediately: a stolen laptop, a shared password, a member removed from a team, a support agent who has left, an account you have just banned."}]},{"t":"p","runs":[{"text":"If your answer is “they stay logged in until it expires”, you do not have a session system, you have a countdown."}]},{"t":"h2","id":"two-designs-and-the-trade-nobody-explains-clearly","runs":[{"text":"Two designs, and the trade nobody explains clearly"}]},{"t":"p","runs":[{"text":"Server-side sessions.","b":true},{"text":" The cookie holds a random opaque ID; the server looks it up in Redis or a table. Revoking is a delete. Reading costs one fast lookup per request. This is the boring design and it is the right default for almost every application, including large ones."}]},{"t":"p","runs":[{"text":"Stateless tokens (JWTs).","b":true},{"text":" The token itself carries the claims, signed. No lookup, which is the entire selling point, and therefore no way to revoke, because nothing is consulted. The usual patch is short-lived access tokens (5 to 15 minutes) plus a long-lived refresh token that "},{"text":"is","i":true},{"text":" stored server-side, so revocation takes effect at the next refresh. That is a reasonable design, and notice what it has done: reintroduced the database lookup you were avoiding, and added a window during which a revoked user is still working."}]},{"t":"p","runs":[{"text":"Choose stateless tokens when you have several services that must verify without a shared store. Choose server-side sessions otherwise. “JWTs scale better” is a claim about an architecture most small products do not have, and the cost is paid in the incident where you cannot log somebody out."}]},{"t":"h2","id":"the-cookie-attributes-and-what-each-one-prevents","runs":[{"text":"The cookie attributes, and what each one prevents"}]},{"t":"code","lang":"","label":"Text","text":"Set-Cookie: sid=<random>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1209600"},{"t":"ul","items":[[{"text":"HttpOnly","b":true},{"text":" keeps JavaScript from reading it, so one cross-site scripting bug does not hand over every session."}],[{"text":"Secure","b":true},{"text":" keeps it off plain HTTP."}],[{"text":"SameSite=Lax","b":true},{"text":" stops the cookie being sent on most cross-site requests, which removes the majority of cross-site request forgery. "},{"text":"Strict","code":true},{"text":" breaks inbound links from other sites; "},{"text":"None","code":true},{"text":" requires "},{"text":"Secure","code":true},{"text":" and re-opens the risk, so use it only for a genuine cross-site flow."}],[{"text":"A random, long ID","b":true},{"text":": at least 128 bits from a cryptographic generator. Not a counter, not a hash of the user ID, not anything derived from time."}]]},{"t":"p","runs":[{"text":"Store a hash of the session ID rather than the ID itself, so a leaked database is not a set of working sessions. This is the same argument as hashing passwords, applied to the thing that substitutes for one."}]},{"t":"h2","id":"rotate-on-privilege-change","runs":[{"text":"Rotate on privilege change"}]},{"t":"p","runs":[{"text":"Issue a new session ID at login, and again whenever authority changes: password change, email change, second factor enabled, role granted. Two reasons: it defeats session fixation, where an attacker plants a known ID before you log in, and it ensures a password change actually ends the attacker’s session rather than leaving it alongside yours."}]},{"t":"p","runs":[{"text":"A password reset should, by default, end every other session. If you offer “log out everywhere”, make sure it reaches API tokens and mobile apps too, or it is a comforting button that does nothing."}]},{"t":"h2","id":"show-people-their-sessions","runs":[{"text":"Show people their sessions"}]},{"t":"p","runs":[{"text":"A list of active sessions (device, approximate location, last seen, with a revoke button) is perhaps two hundred lines of work and it does three things at once: it lets a user end a session you know nothing about, it makes a compromise visible to the only person who can recognise it, and it gives your support process something to point at. Every large service has one because they are the first thing a worried user asks for."}]},{"t":"h2","id":"expiry","runs":[{"text":"Expiry"}]},{"t":"p","runs":[{"text":"There is a real tension. Short sessions are safer and annoy people into choosing worse passwords and staying signed in on shared machines. Long sessions are kind and leave more time for a stolen cookie to be useful."}]},{"t":"p","runs":[{"text":"A defensible middle: a sliding two-week session for ordinary use, an absolute maximum of a few months regardless of activity, and a re-authentication prompt in front of dangerous actions (changing email, deleting the account, viewing payment details) regardless of how fresh the session is. That last one is what protects the user whose unlocked laptop somebody borrowed for a minute."}]},{"t":"h2","id":"the-failure-to-plan-for","runs":[{"text":"The failure to plan for"}]},{"t":"p","runs":[{"text":"Credential stuffing is the common attack and it is not a breach of your system: somebody takes a list of passwords leaked elsewhere and tries them against your login. The defences are rate limits per account rather than only per IP, a check against known-breached password lists (the free range-query API from Have I Been Pwned does this without sending you the password), and a second factor for accounts that can do damage."}]},{"t":"p","runs":[{"text":"When it succeeds, everything above is what you use: revoke the sessions, force a reset, show the user their session list, and tell them plainly what happened."}]}],"check":{"question":[{"text":"A service issues 24-hour JWTs with no server-side store. A user’s laptop is stolen and they ask to be logged out everywhere. What can the service actually do?"}],"options":[{"text":[{"text":"Delete the session record, ending access immediately"}],"correct":false,"tempting":[{"text":"There is no session record; the whole design avoids one, which is exactly why this option is unavailable."}]},{"text":[{"text":"Rotate the signing key, invalidating that token and every other token in circulation"}],"correct":false,"tempting":[{"text":"It does work, and it logs out every user of the service at once: a blunt instrument that most teams find unacceptable, though it is sometimes the only lever available."}]},{"text":[{"text":"Nothing that takes effect before the token expires, unless a deny-list or key rotation is introduced"}],"correct":true,"tempting":null},{"text":[{"text":"Change the user’s password, which invalidates previously issued tokens"}],"correct":false,"tempting":[{"text":"Only if the token verification consults something that changes with the password, which a purely stateless design does not do."}]}],"order":[3,1,2,0],"why":null},"prev":{"slug":"who-is-allowed-to-do-what","title":"Authentication is not authorisation","minutes":9},"next":{"slug":"dependencies-and-the-supply-chain","title":"The code you did not write","minutes":9}}}