NOTES & IDEAS
How Do You Log Someone Out of a Login Token? The One-Number Trick
Login tokens (JWTs) are designed so the server keeps no list of them - which is exactly why "log out everywhere" is surprisingly hard. Here's the one-number trick that fixes it, explained with a hotel keycard.
Here's a question that trips up a lot of developers: "You log users in with a token. A user clicks 'log out of all devices.' How do you actually cancel the tokens that are already out there?"
The honest first answer is: you can't cancel the token itself. And that's not a bug - it's the whole design. Let me explain with a hotel keycard, because that's almost exactly how these tokens work.
Why a login token is like a hotel keycard
When you log in, the server hands you a JWT - think of it as a hotel keycard. Here's the clever, slightly scary part: the door lock keeps no list of which cards it has handed out. When you tap a card, the lock just checks two things: was this card validly made by us, and has it expired? If both are fine, the door opens.
That's what makes it fast and cheap - the lock doesn't have to phone the front desk or look anyone up. But it creates a problem. A guest loses their card, or you want to kick someone out right now. You can't "delete" a card that's already in someone's pocket, because the lock never had a list to remove it from. A valid, unexpired card keeps working no matter what.
That's wonderful for speed and terrible for moments like "log me out everywhere" and "I just changed my password." In Writer's Heaven I closed this gap with one of the cheapest tricks in the book: a single number I call the token version.
The trick: a version number on the lock and the card
Give every user one number, starting at 0 - the lock's current "generation." Two simple rules:
- When you hand out a card (token), stamp it with the user's current number.
- On every tap, check the card's stamped number against the user's current number. If they don't match, the door stays shut.
// runs on every logged-in request
async validate(payload: any): Promise<Auth> {
const user = await this.authService.findById(payload.sub);
if (!user) throw new UnauthorizedException();
// the whole trick: the card's number must match the user's current number
if ((user.tokenVersion ?? 0) !== (payload.tokenVersion ?? 0)) {
throw new UnauthorizedException();
}
return user;
}
That single comparison is the entire mechanism. As long as the card's stamped number matches the user's current number, it works. The instant they differ, every card carrying the old number is dead.
Logging everyone out = adding 1
To cancel every card a user has, you don't touch the cards at all. You just turn the lock's number from 0 to 1:
// "log out of all devices"
await this.users.increment(userId, 'tokenVersion');
Now every card still in the wild says "0," the lock now expects "1," and they all stop working at the next tap. The user goes to the front desk (logs in again) and gets a fresh card stamped "1." Instant, total lockout - by changing one number.
The natural moments to add 1:
- Log out everywhere - the user wants every session killed.
- Password change or reset - if the old password might be compromised, every old session should die. Bumping the number guarantees it.
- Admin kicks someone out - e.g. suspending an account.
What it costs: one quick lookup per request
A pure keycard system needs zero lookups - the lock trusts the card on sight. This trick adds one small lookup per request (fetching the user's current number). That's a real cost, and worth saying plainly.
But look at what you avoid: you are not keeping a giant list of cancelled cards that grows forever and has to be cleaned up. It's one quick lookup of the user - which you very likely needed anyway to load their account. And if that lookup ever gets hot, the user record is easy to cache (and you clear that cache when… you bump the number - it all ties together).
"Why not just make cards expire fast?"
You'll hear "just make tokens expire in 5 minutes." That shrinks the danger window but doesn't close it - a stolen card still works for those 5 minutes, and now everyone's card keeps needing to be re-issued. The two ideas actually work together: short-ish expiry to limit the damage of a leak, plus the version number for the times you need a cancellation to be immediate and certain (password changed, account compromised). It's both, not either-or.
Takeaway
"The server keeps no list" is a feature and a limitation at the same time. The token-version number is the smallest possible scrap of state - literally one number per user - that buys back the one thing these tokens can't do on their own: say "every card I've ever handed this person is void, right now," by changing a single digit. It's a great trick to keep in your pocket, and an even better answer to that interview question.