JWT refresh tokens

A refresh token buys back the one thing a JWT cannot do: withdraw access. It works by splitting the credential so that exactly one half touches your database.

Method note: unlike most guides on this site, nothing here is measured. This is an architecture question rather than a behavioural one, so the claims below follow RFC 6749 and the OAuth security best-practice draft. Where those documents leave a choice open, we say so rather than inventing a number.

Start from what breaks

A JWT is valid because its signature verifies and its expiry has not passed. Neither of those consults your server. That is the property that makes it stateless, and it is also the reason you cannot revoke one. A user changes their password, an employee is terminated, a token leaks from a log — the token keeps working until it expires, and there is no request you can make that changes this.

Shortening the lifetime bounds the damage, but a token short enough to make revocation meaningful is short enough to log people out constantly. The refresh token exists to resolve that tension, and every design decision below follows from it.

The split

access token    minutes    stateless, never checked against a store
refresh token   days       opaque, stored server-side, revocable

    request ──▶ access token verified by signature only
                                    │ expires
                                    ▼
    refresh ──▶ refresh token looked up in database ──▶ new access token
                        │
                        └── delete the row and this stops working

Ordinary requests stay stateless, which is the whole reason you reached for a JWT. Only the refresh exchange hits your database, and that is where revocation lives: one lookup every fifteen minutes rather than one per request. Delete the row and the session ends within one access-token lifetime — a bounded, predictable window rather than whenever the outstanding token happens to expire.

Why the refresh token must not be a JWT

This is the most common mistake, and it comes from assuming both halves should be the same kind of thing. They should not. A refresh token is checked against server state on every single use, so a signature buys you nothing you are not already paying for — and worse, it invites the failure mode where someone trusts the claims inside it and skips the lookup, which quietly removes the only revocation you had.

Use an opaque random value from your platform's secure generator and store its hash rather than the value. A leaked table of refresh tokens is otherwise a leaked set of working credentials. Keep whatever metadata you need — user, device, issued-at, family — in the row, where you can query and expire it.

Rotation, and what it actually detects

Rotation means issuing a new refresh token on every exchange and invalidating the one just used, so each is single-use. The benefit is detection rather than prevention:

legitimate user   refresh(A) ──▶ B    A now invalid
attacker (copy)   refresh(A) ──▶ ✗    A already redeemed

    → two parties hold A. A copy leaked.
    → invalidate the whole family, force re-authentication

Nothing in that second request identifies it as the attacker's — it carries a token that was legitimately issued. So the only safe response is to invalidate the entire family and make everyone sign in again. That is disruptive, and the disruption is the point: without rotation a stolen refresh token works quietly until it expires and you never learn it happened at all.

Choosing the two lifetimes

Both numbers are policy rather than technical limits, and they answer different questions. The access token lifetime is your revocation delay — set it to fifteen minutes and that is the longest anyone keeps access after you cut them off. The refresh token lifetime is how long an inactive user stays signed in before being asked to authenticate again.

Derive them from "how long is too long for someone to keep access after we revoke it" rather than from a number in a blog post, because the right answer depends on what the account can do once compromised. A read-only dashboard and an admin console do not deserve the same window. Remember that both are exp claims measured in seconds, not milliseconds.

Where each half lives

The refresh token belongs in an httpOnly, Secure, SameSite cookie, and this matters more here than for the access token: it lives far longer, so a copy taken from localStorage is useful for far longer, and it is the credential that mints new access tokens.

Scope the cookie's Path to the refresh endpoint so it is not attached to every ordinary request, which cuts both exposure and the header bytes paid on each call. The access token can then live in memory — gone on reload, restored by the cookie. That combination is laid out in where to store a JWT.

Short answers

Should the refresh token itself be a JWT?

No. It is checked against server state on every use, so a signature saves you nothing — you are doing the lookup regardless. Use an opaque random value and store its hash.

How long should an access token live?

Five to fifteen minutes. That number is your revocation delay: it is how long a terminated employee keeps working access after you delete their session.

And the refresh token?

Days to weeks. This one sets how long an inactive user stays signed in, so it is a product decision rather than a security one.

Does rotation stop a stolen token being used?

No — a thief who refreshes first still gets a working token. Rotation buys detection, not prevention. The theft announces itself the next time the other party refreshes.

What should happen when a reused token is detected?

Invalidate the whole token family and force re-authentication. You cannot tell from the request which party is the attacker, so the only safe response affects both.

Where should it be stored?

An httpOnly, Secure, SameSite cookie, with Path scoped to the refresh endpoint so it is not attached to every ordinary request. This matters more for the refresh token than the access token, because it lives far longer and mints the others.

Is exp in seconds or milliseconds?

Seconds. Passing milliseconds produces a token that appears valid for about 50,000 years, which no library will warn you about.

References

Paste a token into the decoder to read its claims and check whether exp is where you think it is. More tools at withuse.io/tools.