Ask why apps use refresh tokens and most people say "because access tokens are short-lived." Okay, but why are they short-lived. That follow-up is the actual question, and it has a specific answer.
I built the auth for my own CMS, access token plus refresh token, Argon2id for password hashing, three roles. So this is the reasoning I actually used.
What's in a JWT, and what happens if it leaks
A JWT is three base64 chunks joined by dots: a header, a payload, and a signature. The payload is where the userId, role, and an expiry go. It is not encrypted. Anyone who has the token can read the payload. The signature doesn't hide anything, it only proves the token was issued by your server and hasn't been edited.
So the honest answer to "what happens if a JWT is stolen": whoever holds it is that user, with that role, until the token expires. Your server has no list of valid tokens to cross-check against. You can't reach out and cancel it. That single fact, a signed token you can't un-issue, is what the rest of the design is working around.
Why two tokens instead of one
If you can't revoke a token, your only lever is time. That's the whole idea.
- Access token: short life, minutes to an hour. Sent on every request. Not stored server-side. If it leaks, the window is small.
- Refresh token: long life, days to weeks. Sent only to one endpoint, the "give me a new access token" one. This one can be stored and tracked server-side, so this one you can revoke. One long-lived token would mean a single leak gives an attacker weeks of access -- no alarm, no off switch -- with no way to stop it. Splitting it means the thing that gets sprayed across every request dies fast, and the thing that lives long barely moves.
Rotation
Better setups rotate the refresh token too: every time it's used, it's swapped for a new one and the old one is retired. If an old refresh token ever gets used again, that's a signal it was stolen, and you can kill the whole chain.

"How would you force-logout a user everywhere?"
Common follow-up, and people freeze because they know access tokens can't be revoked. The answer works around it:
- Keep the access token TTL short (so it self-clears fast)
- Store refresh state server-side, either a row per refresh token, or a
tokenVersionnumber on the user record that gets baked into each token - To log everyone out: delete the refresh rows, or bump
tokenVersion. Now every refresh attempt fails the check, and within one short access-token lifetime they're fully out.
You don't revoke the access tokens. You wait them out, and you slam the refresh door.
RBAC without if (role === 'ADMIN') everywhere
The bad version scatters role checks through every handler. Add a role later and you're grepping the whole codebase.
The maintainable version:
- Roles map to permissions, not to scattered conditionals.
ADMINhasblog:write,blog:delete, and so on. - Route handlers declare the permission they need. One middleware does the check. The handler never mentions a role name.
- Adding a role becomes a data change, extend the role-to-permission map, not a code change across twenty files.
My CMS has three roles, SUPER_ADMIN, ADMIN, AUTHOR, checked in a single auth middleware. A route says "this needs an author or above," and that's the only place the hierarchy lives.
Quick recap
- A JWT payload is readable base64, not encrypted, and a stolen one can't be individually revoked
- Access tokens are short-lived because time is your only defense against a token you can't cancel
- The refresh token is the revocable half: long-lived, sent to one endpoint, tracked server-side, ideally rotated
- Force-logout works by killing refresh state (rows or a
tokenVersion) and letting short-lived access tokens expire on their own - RBAC scales when roles map to permissions checked in one place, so a new role is a data edit, not a code hunt
The version that passes an interview isn't "refresh tokens are more secure." It's "you can't un-steal a JWT, so you make the exposed one cheap to lose and keep the valuable one where you can reach it."




Comments
No comments yet — be the first to share your thoughts.