What sessions and tokens are, why 30-day tokens and a logout that ended nothing come from one cause, and the checklist that fixes it in any app.
If someone copies one of your users’ login tokens, how long can they use it? And can you stop them? At BHyve, the honest answers used to be 30 days and no.
This post explains sessions and tokens in plain words, why ours were weak, the single cause behind it, and the checklist I used to fix it. Every item is a small change. Together they decide whether a leaked token is a disaster or a non-event.
A session is the server’s memory, a token is your key card
A session is the server’s record that you signed in. It starts when you log in. It should end when you log out or stop using the app.
A token is the thing your browser carries to prove it belongs to that session. It goes along with every request, so you don’t have to type your password each time.
A hotel is a good way to picture it. The front desk checks your ID once and hands you a key card. After that, the door only checks the card. Now imagine the card works for 30 days, and the hotel keeps no list of active cards, so it can’t cancel one. Anyone who finds that card has a room for a month.
Many apps use self-contained tokens. A JWT (JSON Web Token) is the common kind: a signed note that says who you are and when the note expires. The server checks the signature and the expiry without looking anything up, which is fast. The catch is the key card again. Until it expires, a valid token is a valid token.
Tokens leak in ordinary ways: malware on a laptop, a token pasted into a log, a shared computer nobody signed out of. You can’t stop every leak. You can decide how long a leaked token lives and whether you can kill it.
What was weak, and why it mattered
Our sessions had the hotel’s problem. Tokens lasted 30 days. Logging out didn’t end anything on the server. Sessions never timed out when nobody used them, and one account could hold as many as it liked. One-time passwords had no limit on guesses.
Any one of these is a gap. Together they meant a copied token was good for a month, and nothing we could do would stop it early.
The cause: the server couldn’t say no
Each weakness looked like its own missing feature. Put side by side, they had one cause. The server kept no record of which sessions were live. It could check that a token was genuine and unexpired, and nothing more.
That one gap explains the rest. To log someone out, the server has to mark their session as ended. To time out an idle session, it has to know when the session was last used. To cap sessions, it has to count them. With no record, it could do none of these, so expiry was the only way a session ever ended. And expiry was a month away.
So I fixed the cause instead of each symptom. The principle is the part worth copying: the server, not the token, decides whether a session is alive. Give the server a list of live sessions, keep tokens short so the list gets consulted often, and logout, timeouts and caps become small features on top.
A session registry lets the server say no
A session registry is a list, kept on the server, of every session that is currently live. It’s the hotel’s list of active key cards. With it, the server can ask a second question after “is this token genuine?”: “is this session still on the list?” Everything else here depends on that question, so build this first.
Short tokens shrink the window
A stolen token works until it expires, which makes lifetime the biggest number in session security.
Users don’t want to sign in every hour, so the usual pattern is that the app swaps an expiring token for a fresh one in the background, but only if the server agrees the session is still on the list. That’s what gives the registry teeth. The shorter the token, the sooner an ended session stops working everywhere.
Logout has to end the session on the server
OWASP’s session management guidance says an application must invalidate the session at least on the server side when the user logs out. Clearing the browser’s copy is a nice extra. It isn’t the logout.
With a registry, logout deletes the session’s entry. Any copy of the token, anywhere, stops working the next time the server checks the list. If the server checks on every request, that’s immediately. If it only checks when a token is renewed, the short lifetime bounds the gap. This is why short tokens and a registry work as a pair.
An idle timeout ends sessions nobody is using
An idle timeout ends a session that hasn’t been used for a while, so a laptop left signed in overnight is signed out by morning.
OWASP says every session should have one, enforced on the server. A timer in the browser is easy to get around, because the browser belongs to whoever is holding it. The server records when each session was last used and refuses it once it has been quiet too long. The right number depends on your app. A banking app and a note-taking tool shouldn’t share one. Pick it on purpose and write down why.
A session cap limits how much can leak
Without a cap, one account can collect dozens of live sessions: an old work laptop, a phone, a browser on a borrowed machine. Each is a token that could be copied. A cap bounds that number. It also keeps the list short enough to show people, which OWASP recommends: let users see their active sessions and end them remotely.
Decide what happens when the next session arrives. OWASP’s guidance on simultaneous logins describes two ways to handle a new one: quietly end an older session, or ask the user which to keep. The same choice applies to a cap. Either is fine. Leaving it undefined isn’t.
Rate limits make one-time passwords hard to guess
A one-time password (OTP) is a short code, sent by email or text message, that works once. Short codes are easy to type and, without limits, easy to guess.
A six-digit code has a million possible values. A script with no limit can work through those quickly. With 5 tries per 15 minutes, each window gives an attacker 5 chances in a million.
A few details decide whether the limit actually holds:
- Count attempts per account, not only per IP address. An attacker can spread guesses across many addresses.
- Update the counter in one indivisible step. If ten guesses arrive at once and the counter only goes up by one, the limit is fiction.
- Prefer a wait over a permanent lockout. A hard lockout lets anyone lock a real user out of their own account.
- Give each code a short life, and make it work only once.
The checklist to copy
Run through this for any app that signs people in:
- The server keeps a session registry. It knows every live session and can refuse one. Build this first, because most of the list depends on it.
- Tokens are short-lived. Minutes to an hour, renewed in the background only while the session is still on the list.
- Logout ends the session on the server. Clearing the browser is extra, not the logout.
- There is an idle timeout, enforced on the server. Pick the number for your risk and write down why.
- Sessions per user are capped. Decide what happens when the next one arrives, and let users see and end their sessions.
- One-time passwords are rate-limited per account. Few tries per window, counters that can’t be raced, short-lived single-use codes.
What it changed
A copied token used to work for 30 days, and nothing could stop it. Now it lasts an hour at most, and once the server ends the session, at logout or after 8 hours without use, it can’t be renewed. Each account holds at most 5 live sessions, and one-time passwords allow 5 tries per 15 minutes. All of it rests on one list the server now keeps.
The lesson
Most session weaknesses come from one gap: a server that can’t end a session. Close that gap first, and the rest of the checklist gets short.
The rest of the security work is in the security case study. Uploads were a different class of problem, covered in safe file uploads. And the session is where your app should learn which customer someone belongs to, which matters a lot in multi-tenant data isolation.
Quick answers
- What is the difference between a session and a token?
- A session is the server's record that you are signed in. A token is the piece of data your browser sends with each request to prove which session it belongs to.
- Does clearing the browser's token log a user out?
- No. It only removes the browser's copy. Any other copy keeps working until the server ends the session, which is why OWASP says logout must invalidate the session on the server.
- What is an idle timeout?
- An idle timeout ends a session after a set time with no activity. OWASP says every session should have one and that the server, not the browser, must enforce it.
- Why should access tokens be short-lived?
- A stolen token works until it expires. A short lifetime, such as an hour, shrinks that window, and the app can renew the token in the background while the session is still active.
- How many one-time password attempts should an app allow?
- Only a few per time window, counted per account. OWASP asks for strict attempt limits without fixing a number. A limit such as 5 tries per 15 minutes makes guessing a short code impractical.