A correct password is only the beginning. Everything after sign-in — whether to verify again, how tokens renew, what happens to old sessions when the password changes — pulls between security and convenience. We wrote our account system's rules as a list where every line can be explained; this article covers the security-facing part.
Know your four credentials
Confusion usually starts with treating all credentials as one thing. Our model has four, managed independently:
| Credential | Purpose | Lifetime | How it dies |
|---|---|---|---|
| Access token | Carries identity per request | Short | Expiry / version bump |
| Refresh token | Exchanges for a new access token | Longer, single-use | Rotated on use; old one dies instantly |
| Device token | Marks a trusted device, skipping the code step | Long, revocable alone | Removed from the account page |
| User version | The master invalidation switch | Lives with the user | Bumped on password change / security events |
Treat the table as a design contract: what it governs, how long it lives, how it is revoked — all three questions answered for every credential.
Trusted devices: skipping verification needs boundaries
Signing in from an unrecognised device requires an email code (a second factor). After verifying, the user may tick "remember this device", and later sign-ins skip the code. The rules live entirely in the boundaries:
- The device token is a separate credential — revoking the device is not signing out the session, and signing out does not touch device trust;
- Users can see every trusted device on the account page and remove them one by one — the revoke path must be visible, or "trust" becomes a black box;
- "Remember device" affects only the code step; password checks and rate limits are unchanged.
One implementation note worth copying: bind the device token to a digest of browser traits, not a full fingerprint — when traits churn, prefer one extra verification over locking a user out.
Token rotation: old tokens must die cleanly
Short-lived access tokens with a refresh token are standard. What is easy to get wrong is when the old token dies:
- On a successful refresh, the previous refresh token is invalidated immediately — one refresh, one renewal, no reuse;
- If someone later presents that retired token, it is a leakage signal — revoke the entire session chain (all tokens for that device/session), not just refuse the request;
- Make it an acceptance criterion: after one refresh, replaying the old refresh token must fail.
Password change, every session out: one version number
Changing the password invalidates every existing token at once — including sign-ins on other devices. The implementation avoids enumerating sessions (easy to miss, easy to lag) in favour of a user-level version:
Tokens carry the version they were issued under
Verification: token version == current version, or refuse
Password change: current version += 1 → every old token dies at once
The trade is a little convenience (every device signs in again) for a definite semantic: after "I think I was compromised, I changed my password", the effect is immediate and explainable. The same switch covers kindred events (suspected leakage, admin freeze) — one mechanism, one meaning.
Guarding the code step itself
Email codes need rules of their own: short expiry, a cap on wrong attempts, a resend cooldown, and rate limits by both email and IP. Four numbers, configured independently — stretching any one leaves room for brute force; squeezing too hard punishes people who simply mistyped. Our calibration principle: make automation uneconomical, keep retries possible for humans.
Takeaways
The engineering quality of account security is that every rule can be explained: which credential does what, when it expires, how the user revokes it themselves. A "security design" that cannot be explained is not security — it is luck. A list that can be explained line by line is what deserves to ship.
