Implementing Refresh Tokens Safely in Production
Field-proven guidance on issuing, rotating, storing, and revoking refresh tokens without introducing session-hijack security holes for your API.
Netbay Cloud Team
Netbay Engineering
On this page
Short-lived access tokens are great for lowering the blast radius of a leak, but they break long-lived client sessions unless you add another layer: the refresh token. Done carelessly, refresh flows create replay and rotation hazards that are worse than the problem they solve. This post covers the safe shape of a refresh token implementation.
Why refresh instead of long tokens
An access token that lives for hours is a pass that works until it expires, even after you detect a leak. Refresh tokens let you keep access short (minutes) while allowing a client to silently get fresh tokens without re-authenticating. The safety knob is that refresh tokens are higher value — they must be stored secret and bound to a single client.
The whole design hinges on three rules: rotate the refresh token on every use, store a fingerprint of the client, and make revocation immediate.
Core safety rules
- **Rotate on use.** Each refresh issuance produces the new refresh token and invalidates the old one. This turns a stolen token into a single-use object.
- **Store only a hash.** Persist a random hash of the refresh token, never the raw value, so a database leak does not leak live sessions.
- **Bind to the client.** Record a stable device or client identifier against the token and reject mismatches.
- **Support immediate revocation.** Keep an allow-list, or short-lived family IDs with a denylist for active breaches.
Two more structural choices are worth making explicit. First, group refresh tokens into a *family*: a single login issues a family id, and every rotation stays inside that family. That is what lets you revoke the whole session at once when one token is reused by an attacker — you do not chase individual records, you kill the lineage. Second, decide where the server keeps these tokens. A durable database is right, but an encrypted cache can be faster if you accept that a cache eviction silently logs the user out; understand that trade-off rather than inheriting it by default.
Rotation shutdowns replay attacks
The classic replay scenario: an attacker steals a refresh token, then both the attacker and the legitimate client try to use it. Without rotation, both succeed — an indefinite hijack. With rotation and reuse detection, the first to use it wins, and the server revokes the whole refresh token family on the second use. This is the single most important hardening step.
The trade-off is that offline, long-lived mobile clients must be able to persist the refreshed token safely (keychain or encrypted storage), and you accept that a genuine client retry could look like a replay if a response is lost. Handle idempotency and client retries explicitly.
A rotation check in Python
import hashlib, secrets
token_hash = lambda t: hashlib.sha256(t.encode()).hexdigest()
def rotate_refresh(old_refresh, client_id, store):
h = token_hash(old_refresh)
record = store.get(h)
if record is None:
raise PermissionError("unknown refresh token")
if record["client_id"] != client_id:
raise PermissionError("client mismatch")
new_refresh = secrets.token_urlsafe(48)
store.delete(h)
store.set(
token_hash(new_refresh),
{"client_id": client_id, "family": record["family"]},
)
return new_refresh, mint_access(record["user"], client_id)Revoking a session on logout
async function revokeRefresh(db, rawToken) {
const hash = crypto.createHash("sha256")
.update(rawToken).digest("hex");
const r = await db.get("refresh", hash);
if (!r) return { ok: false };
// optionally revoke the whole family
await db.put("revoked_family", r.family, { value: Date.now() });
await db.del("refresh", hash);
return { ok: true };
}Handling client retries and loss
Refresh responses that fail mid-flight can desync client and server. Have the client treat a 401 on the refresh call as cause to hard-delete the session, and give the server an idempotent hook (a request id) so a replayed refresh due to a lost response is not mistaken for theft. Document the exact behavior in your client SDK.
Takeaway
Refresh tokens buy you short access lifetimes, but only if they are rotated, stored as hashes, bound to a client, and revocable. Ship those four rules together or not at all. You can build and stress-test this flow against an API-backed VPS on Netbay, in production-ready SSD storage from DC01 in Lucknow — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance