blog/security/refresh-tokens-why-access-tokens-expire
Security & Authentication

Refresh Tokens: Why Your Access Token Should Expire

A stolen access token that lives for 30 days is a 30-day breach. Make it live 15 minutes and you have a new problem: users logged out constantly. Refresh tokens resolve that tension — short-lived access, long-lived renewal — but only if you handle rotation and theft detection right.

·7 min read

The Trade-off Hiding Inside "Stay Logged In"

Give a user an access token that's valid for 30 days and one thing is convenient: they never get logged out. But if that token leaks — copied from a log, stolen via XSS, grabbed off a compromised device — the attacker has 30 days of full access, and with a stateless JWT you often can't revoke it.

So shorten it. Make the access token valid for 15 minutes. Now a stolen token is nearly worthless. But your users are furious, because they get bounced to the login screen every 15 minutes.

You want both: tokens that expire fast and sessions that last for weeks. Those goals fight each other — unless you split the job across two tokens. That's the entire reason refresh tokens exist.


Two Tokens, Two Jobs

📌 Access Token vs Refresh Token

An access token is the key you present on every API call. It's short-lived (minutes) so a leak has a tiny blast radius. A refresh token is a longer-lived credential whose only job is to obtain new access tokens. It's sent rarely — only to the auth server, only when the access token expires — so its exposure surface is far smaller.

The division of labor is the whole trick. The token that travels constantly (access) barely lives. The token that lives long (refresh) barely travels.

PropertyAccess TokenRefresh Token
LifetimeMinutes (5–15)Days to weeks
Sent withEvery API requestOnly to the auth server, on renewal
Exposure surfaceLarge — hits many servicesSmall — one endpoint
If stolenExpires almost immediatelyDangerous — can mint access tokens
StorageMemory (avoid localStorage)HttpOnly cookie / secure store
RevocableHard if stateless JWTYes — server tracks it

The Renewal Flow

When the access token expires, the client doesn't send the user back to login. It quietly exchanges the refresh token for a fresh access token.

Silent Renewal
Client
access expired
refresh token
Auth Server
verify refresh
New Tokens
access (+refresh)
API
call resumes

To the user, nothing happened. Under the hood, the access token was replaced without a login. The refresh token made a 15-minute access token feel like a permanent session.

But notice what we just created: a long-lived credential that can mint access tokens. If that leaks, the attacker has a renewable source of access. So the refresh token needs its own defenses.


Rotation: Don't Reuse Refresh Tokens

The naive design hands out one refresh token and reuses it forever. If it's ever stolen, the attacker refreshes indefinitely and you have no way to tell them apart from the real user.

Refresh token rotation fixes this: every time a refresh token is used, it's invalidated and a new one is issued alongside the new access token. Each refresh token is single-use.

1

Use rotates the token

Client presents refresh token RT1. Server issues a new access token and a new refresh token RT2, and marks RT1 as used.

2

Old token is now dead

RT1 can never be used again. The client discards it and holds RT2 for next time.

3

A reused old token is a red flag

If RT1 shows up again, something is wrong — the legitimate client already moved on to RT2. The only way RT1 gets replayed is if it was stolen.

This is where rotation becomes a detection mechanism, not just hygiene.


Reuse Detection: Turning a Leak Into an Alarm

Because each refresh token is single-use, a replayed token is proof that two parties hold the same token — the real user and an attacker. You can't tell which one is replaying, but you don't have to.

🔴 On reuse, revoke the whole family

When a refresh token is used a second time, invalidate the entire token chain descended from that login — every refresh and access token in the family. Both the attacker and the real user get logged out. The user re-authenticates (annoying but safe); the attacker is locked out (the point). A single-use token that gets reused is the clearest theft signal you'll ever get — act on it.

Reuse Detection
Attacker
replays RT1
stolen RT1
Auth Server
RT1 already used
Revoke Family
kill all tokens
User
must re-login

This is the payoff of the whole design. Short-lived access tokens shrink the blast radius; rotation plus reuse detection turns a stolen refresh token from a silent, open-ended breach into a self-tripping alarm.


Design Decisions You Still Have to Make

Refresh tokens don't come with one right configuration. A few choices depend on your context:

Where Should the Refresh Token Live?

What kind of client is it?

Other knobs:

  • Sliding vs absolute expiry. A refresh token can slide (extend on each use, so active users stay logged in indefinitely) or have an absolute max lifetime (force re-auth after, say, 30 days no matter what). Sliding is friendlier; absolute is safer. Many systems do both — slide up to a hard ceiling.
  • Stateful refresh tokens. Unlike access JWTs, refresh tokens are usually stored server-side (or as opaque references) precisely so you can revoke them. That's a deliberate reversal — you accept a database lookup on refresh to regain revocation control.

⚠️ Don't make the access token stateful too

The reason access tokens are short-lived JWTs is so services can validate them without a database call. If you find yourself checking a revocation list on every access-token request, you've thrown away the performance benefit — shorten the access token's lifetime instead and let it expire on its own.


Key Takeaways

  • Access and refresh tokens split one job in two: short-lived access (tiny blast radius, travels everywhere) and long-lived refresh (renews sessions, travels rarely).
  • The refresh token lets you keep access tokens short without logging users out — silent renewal replaces the token, not the session.
  • Rotate refresh tokens (single-use) and detect reuse: a replayed refresh token means it was stolen, so revoke the whole token family and force re-login.
  • Store refresh tokens where scripts can't read them (HttpOnly cookies, OS secure storage), keep them server-revocable, and cap their lifetime with an absolute ceiling.

References