JWT refresh tokens

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

The problem it solves

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

Shortening the lifetime bounds the damage but logs people out. The refresh token resolves that tension by splitting the credential in two.

How the split works

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. Only the refresh exchange hits your database, and that is where revocation lives. Delete the row and the session ends within one access-token lifetime — fifteen minutes, not whenever the token happened to expire.

The refresh token should not be a JWT

This is the most common mistake, and it comes from assuming both halves should be the same kind of thing. A refresh token is checked against server state on every use, so a signature saves you nothing — you are doing the lookup anyway.

Use an opaque random value from your platform's secure generator, and store its hash rather than the value. A leaked database 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 not prevention — a thief who refreshes first still gets a working token — but detection:

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

You cannot tell from the request which party is the attacker, so the only safe response is to invalidate the entire family and make everyone sign in again. That is disruptive, which is the point: without rotation a stolen refresh token works quietly until it expires, and you never learn it happened.

Choosing the lifetimes

TokenTypicalWhat the number means
Access5–15 minutesyour revocation delay — how long access survives a logout
Refreshdays to weekshow long an inactive user stays signed in

Both are policy, not technical limits. Derive them from the question "how long is too long for someone to keep access after we revoke it" rather than from a number in a blog post. Remember that these are exp claims measured in seconds, not milliseconds.

Where to put it

An httpOnly, Secure, SameSite cookie, and this matters more here than for the access token. The refresh token 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 header size. The access token can then live in memory, gone on reload and restored by the cookie. That combination is covered in where to store a JWT.

Frequently asked questions

Why do I need a refresh token at all?

Because it is the only way to get revocation back. A JWT is valid until it expires and nothing consults your server in between, so a short lifetime is your only control over a leaked one — but a short lifetime means logging users out every few minutes. The refresh token resolves that: a short-lived access token carries ordinary requests statelessly, while a longer-lived refresh token is stored server-side and exchanged for new access tokens. That exchange does hit your database, which is precisely where you can refuse — one lookup every fifteen minutes rather than one per request. Delete the record and the session ends within one access-token lifetime rather than whenever the outstanding token happened to expire, which you cannot predict.

Should a refresh token be a JWT?

No, and making it one misses the point. A refresh token is always checked against server state, so there is nothing for a signature to save you — you are doing the lookup regardless. Use an opaque random value from your secure generator, store its hash rather than the value so a database leak does not hand over working credentials, and keep whatever metadata you need in the row rather than in the token. This is the same reasoning that makes a UUID a poor password reset token: once you are storing state anyway, the format buys you nothing, and a signed refresh token invites the mistake of trusting its claims without the lookup.

What is refresh token rotation?

Issuing a new refresh token on every exchange and invalidating the one just used, so each token is single-use. The security benefit is detection rather than prevention: if a token that has already been redeemed is presented again, two parties hold it, which means a copy leaked. You cannot tell which party is the attacker, so the correct response is to invalidate the entire token family and force re-authentication, disruptive as that is. Without rotation a stolen refresh token works quietly until it expires; with it, the theft announces itself the next time either party refreshes, which is usually within minutes rather than days after the theft.

How long should each token live?

Access tokens in minutes and refresh tokens in days or weeks, with the exact numbers following from what you can tolerate. The access token lifetime is your revocation delay — set it to fifteen minutes and a terminated employee retains access for at most fifteen minutes. The refresh token lifetime is how long an inactive user stays signed in. Both are policy rather than technical limits, so pick them from the question "how long is too long for someone to keep access after we revoke it" rather than from a number someone posted online, since the right answer differs by what the account can do once compromised.

Where should the refresh token be stored?

In an httpOnly, Secure, SameSite cookie, and this matters more for the refresh token than for the access token. It lives far longer, so a copy stolen from localStorage is useful for far longer, and it is the credential that mints new access tokens. Scope its cookie path to the refresh endpoint so it is not attached to every ordinary request, which reduces both exposure and the header size paid on each call. The access token can then live in memory, gone on refresh and restored by the cookie — the shape covered in our storage guide, where the access token never touches persistent storage at all and dies with the tab.

References

Inspect a token's claims and expiry with the JWT decoder. More tools at withuse.io/tools.