Access and refresh tokens

BackendTier 2 · build and shipPhase 3 · Give customers accounts

The modern proof of identity: a short-lived access token you send with each request, plus a longer-lived refresh token that quietly renews it.

After a login succeeds, every later request still has to prove it belongs to that user, without sending the password again and again. Rechecking credentials constantly is slow and risky. Access and refresh tokens exist as short-lived proof the client carries on each request, so the server can trust it quickly and refresh or revoke it without forcing a new login.

After login, the server returns an access token, a signed string that proves who you are. Your app sends it with every request (in an Authorization header). The server checks it and trusts the request. Crucially, the token expires quickly, in minutes to hours, to limit damage if it is stolen.

So you are not logged out constantly, the server also issues a refresh token that lives much longer. When the access token expires, the app silently uses the refresh token to get a fresh one. This is why you stay signed in for weeks yet each individual token is short-lived.

A junior PM, stuck

Security flagged that someone pasted a staging URL ending in ?token= into a group chat, and since it is my feature they asked me to assess how bad the leak is. The token is a wall of random letters and I cannot tell whether it exposes anything real or how long it stays usable. I do not want to declare an incident over a string I cannot even read.

That wall of letters is a JWT, a JSON web token, and two of its three parts are readable with nothing more than a base64 decoder. You can answer security's question yourself in about five minutes. Here is the exact token from that URL, split at its two dots, with the middle part decoded underneath, and next to it the refresh token from the same login, because which of the two leaked is the whole assessment.

The access token from the leaked URL, and the refresh token from the same login
Access token, split at its two dots
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1Xzg4MjMxIiwibmFtZSI6IlRhc21lZXQifQ
.s1gn4tur3_n0t_r34l
Middle part, base64 decoded
{
"sub": "u_88231",
"name": "Tasmeet"
}
Refresh token, from the same login response
dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4gZm9yIHRpZmZpbmJveA

Click a step to see the lines it points at.

Calling every leaked token a full breach. A 15-minute customer access token on staging and a 30-day refresh token in production deserve different responses, and treating them the same burns your credibility with security.
Assuming a token is safe because it looks encrypted. The middle part is an open book to anyone with a base64 decoder, so any user id, email, or role a dev puts in the payload is public the moment the token leaks.
Pasting the real token into tickets and chats while investigating the leak. You become the second incident.
Asking devs to make tokens last longer when users complain about logouts. The short life is the security feature. Mid-session logouts usually mean the refresh flow is failing, which is a different ticket.

Reply to security with: "The leaked string is a staging access token for one customer account, u_88231, with no admin claims. It expires 15 minutes after issue and cannot renew itself, so the exposure window has closed, and staging holds no live payment keys. I confirmed the 30-day refresh token was not in the URL; if it had been, I would be asking you to force a re-login on that account." You decoded the payload, checked the lifetime, and separated the scary-looking string from the genuinely dangerous one. That is the whole skill.

"Users get logged out mid-session" can mean refresh is failing; "a leaked link logged someone in" means a token ended up somewhere it shouldn't.
Token lifetime is a security-versus-convenience dial you will discuss for any logged-in product.

"Access tokens last 15 minutes, refresh lasts 30 days, that's the standard setup."

Appears in Phase 3, Give customers accounts.