Signed out everywhere, except one phone
Noor's phone is stolen at a concert. On a friend's laptop she opens her ticket app and presses Sign out of all devices. Whether the thief can still transfer her tickets was decided long before: by what the app sends with each request after the login.
HTTP remembers nothing between requests, so every request after the login must carry proof that it belongs to Noor's login. How the login proves who she is, with passwords, second factors or an identity provider, is the ground of Authentication: passwords, MFA & identity providers, accepting a sign-in from a provider someone else runs that of Federated Identity, and the redirects that hand an app its tokens that of OAuth 2.0 & OpenID Connect flows. Here we start at the credential the login hands back. There are two ways to build one, and they differ in where the truth lives.
A random id and a row
The first is a server-side session. At login the server writes a row to a session store, keyed by a session id: at least 128 random bits from a cryptographic generator, so guessing a valid one is impractical. The row says who the user is and when the session ends; the browser gets only the id, in a cookie. The id is opaque: it means nothing by itself, and only a lookup says whose it is.
- The app sends
GET /ticketswith the cookiesid=9f2c…. - The API looks up
9f2c…in the session store and finds Noor, valid until 23:40. - The API answers with her tickets.
Logout deletes the row, and the next request with that id gets a 401. The cost is step 2: every request, on every API instance, needs the store. So the store is shared (Load balancing & horizontal scaling shows why sticky sessions are not the fix) and replicated, since a store that dies logs everyone out. An idle timeout that each request extends keeps a stolen session alive too, so an absolute cap, say 30 days from login, bounds it.
A note the server signed
The second way moves the truth into the credential. A signed token carries the facts about the user and a signature proving the issuer wrote them. The common format is the JWT (JSON Web Token): a header, a payload of claims and the signature, joined by dots. Noor's carries sub (the subject, user 81734), iss (the issuer, the auth service), aud (the audience, the tickets API) and exp (the expiry).
A typical JWT is signed, not encrypted: anyone holding it can read the claims. What the signature stops is a change, and only if the API checks all of this:
- The signature, with a key the API already has. With an asymmetric algorithm such as RS256 or ES256, the auth service signs with a private key and APIs verify with the public one, so a breached API cannot mint tokens. With HS256 one shared secret does both, so every holder can mint them.
- The algorithm, fixed by the API, never taken from the token's header. Libraries that trusted the header have accepted
alg: none, a token with no signature. exp, with at most a small allowance for clock drift.issandaud, so a token from another issuer, or meant for another service, is refused.
None of these checks needs a store. Rotating the signing key fails every token signed with the old one unless APIs keep accepting the old key until those tokens expire; where signing keys are kept and how they rotate is in Secrets management & encryption. A valid token says which account the call acts for, not what it may do; Authorization: roles, relationships & policies takes it from there.
A day of valid signatures
Back to Noor's logout. With sessions, her rows are deleted and the phone's next request fails. With a 24-hour JWT, nothing the API checks has changed: the signature is valid, exp is in the future, and the thief can transfer her tickets for up to a day.
A verifier that consults only the token cannot learn it was revoked; consulting something on every request brings back the lookup. The reference design for Distributed Session Store states the trade: sessions cost a lookup per request and give instant revocation; stateless JWTs skip the lookup and give up exactly that.
Short access, long refresh
The usual answer splits the credential in two. An access token, here a JWT, lives for minutes, say 10, and every API verifies it locally. A refresh token lives for weeks and goes only to the auth service, which keeps a record of it, ideally a hash, so a leaked table holds no usable tokens. When the access token expires, the app sends the refresh token and gets a new access token once its record checks out.
The tickets API verifies access tokens with the public key alone. Only a refresh reaches the token records, where a revocation takes effect.
When Noor signs out everywhere, the auth service revokes her refresh tokens. The phone's access token works until its exp, at most 10 minutes later, and its next refresh fails. That lifetime is the exposure window you choose: shorter means faster revocation and more refresh traffic.
What a reused refresh token tells you
A weeks-long token is worth stealing. Refresh-token rotation makes each one single-use: every refresh also returns a new refresh token, and the old one is marked used in the same atomic step, so two refreshes racing with one token cannot both pass as the first. The tokens descended from one login form a family.
Suppose a copy leaks from a log line and the thief refreshes with it. When the real app later presents the same, already used token, the auth service cannot tell which caller is the thief, only that one exists, so it revokes the whole family: neither can refresh again, and once their current access tokens expire, both must log in again. Rotation catches a copied token once both copies are used; it did nothing for Noor, whose thief holds the only copy, so her case needs the explicit revocation above. Two tabs refreshing at once look exactly like theft, which is why some implementations accept the previous token for a few seconds more.
When ten minutes is too long
SSO Identity Provider asks to revoke a session everywhere within seconds. Then the APIs must consult something after all:
- A denylist of revoked token ids (the
jticlaim), checked on every request. An entry only stays until its token'sexp, so the list holds only the revocations of one access-token lifetime, but every request pays a lookup again. - A per-user cutoff. The auth service stores a cutoff time per user, and APIs refuse that user's tokens issued before it (the
iatclaim). It is easy to cache and push, and once an API has the new value, all of that user's older tokens fail there. It ends all of that user's sessions at once, so it fits Sign out of all devices, not a single one.
Where the browser keeps it
An HttpOnly cookie keeps the credential out of reach of any script in the page, but the browser attaches it to requests by itself; a token in localStorage goes out only when the app's code adds it, and any script on the page can read it. Which attack each choice leaves open, and how to close it, is worked through in Web application security: injection, XSS, CSRF & SSRF.
The copy that missed the logout
Back to sessions. The ticket API runs on 12 instances, and to spare the store, each keeps the sessions it has just read in memory for 60 seconds. Noor's laptop deletes her rows, but the instance that served her phone a moment ago accepts its copy for up to a minute. The cache has turned the session into a short-lived token.
App #1 deletes the rows and publishes the logout. App #2 drops its cached copy of the phone's session.
That broadcast is what the reference design draws, with one weakness: a plain pub/sub message reaches only the instances listening at that moment, so the short local lifetime remains the bound for one that missed it, and for a read already in flight that caches the session again just after the drop. The store has a gap of its own. With asynchronous replication, a logout acknowledged just before a failover can be lost and the session comes back until it expires, which is why logout writes are worth making wait for a replica's acknowledgement; that holds only if the failover promotes a replica that received it.
How long is the phone still in?
Ask it of a Distributed Session Store Grid, whose brief wants replicated sessions with a TTL and a logout that reaches every node's local copy. Follow Noor's logout through your design, put a number on how long the stolen phone is still served, and name what sets it: the local cache, the broadcast or the replication. The token half has its own brief, SSO Identity Provider, where rotating refresh tokens meet a revocation deadline of seconds; start by choosing which of the two checks above meets it.