A refresh token is a long-lived credential a client exchanges for short-lived access tokens; rotation issues a new refresh token on each use and invalidates the previous one; revocation is the server’s ability to end a session before its token would naturally expire.
In simpler words
Instead of one long-lived login token, you keep a short access token that expires fast and a refresh token that quietly gets you a new one — and the server can cut either off early if something looks wrong.
A common baseline issues a single access-token cookie that lives for a fixed expiry (say 7d) with no refresh and no server-side revocation. Use that as the baseline: understand what a refresh + rotation flow adds and why production auth usually wants it.
Explain why a short access token plus a refresh token beats one long-lived token
Describe refresh-token rotation and reuse detection
Name the server-side state a revocable session needs
A single-token baseline versus the refresh model
Definition
A refresh flow splits authentication into a short-lived access token used on every request and a long-lived refresh token stored separately and sent only to a dedicated refresh endpoint, so a leaked access token expires quickly while sessions still last for days.
In simpler words
In this baseline one 7-day cookie does both jobs; splitting them means a token stolen from a request only works for minutes.
AuthService.login here signs one JWT with a multi-day maxAge and never reissues it — if that token leaks, it stays valid until it expires because nothing can revoke it.
A refresh model shortens the access token to minutes and adds a POST /auth/refresh route that reads the refresh token and mints a fresh access token, so the high-value credential is used rarely and on one path only.
Baseline: one long-lived token
// One token, ~7 days, no refresh, no revoke
const token = await this.jwtService.signAsync({ sub, email, role });
res.cookie(AUTH_COOKIE_NAME, token, { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 });
Simple and fine for a course, but a stolen token stays valid for a week with no way to cut it off.
The refresh cookie is scoped to the refresh path so it is not attached to every ordinary request.
Rotation and reuse detection
Definition
Refresh-token rotation replaces the refresh token every time it is used and marks the old one as spent, so a refresh token is valid exactly once; if a spent token is presented again, the server treats it as theft and revokes the whole session family.
In simpler words
Each refresh hands you a new refresh token and burns the old one — if an old one shows up again, someone copied it, so you log the session out entirely.
Store a hashed refresh token (or a token id + version) per session; on refresh, verify it matches the current one, then issue and persist a new one.
Reuse detection is the payoff: if both an attacker and the user hold a copy, one of them presents an already-rotated token, and that mismatch is the signal to revoke the family.
Rotation with reuse detection (sketch)
async refresh(presented: string) {
const session = await this.sessions.findByToken(hash(presented));
if (!session || session.revokedAt) throw new UnauthorizedException();
if (session.currentTokenHash !== hash(presented)) {
await this.sessions.revokeFamily(session.familyId); // reuse detected
throw new UnauthorizedException('Token reuse detected');
}
const next = await this.issueRefreshToken(session.userId, session.familyId);
await this.sessions.rotate(session.id, hash(next));
return next;
}
Storing only the hash means a leaked database still does not hand out usable refresh tokens.
Revocation and logout-everywhere
Definition
Because a pure JWT is valid until it expires, ending a session early requires server-side state — a per-session record or a per-user token version — that guarded requests consult so a logout, password change, or admin action can invalidate tokens immediately.
In simpler words
Revocation is the thing plain JWTs cannot do alone: you need a small server-side list (or version number) to say “this session is over now.”
Logout today only clears the cookie — the token string itself would still verify if copied. Revocation needs a stored session you can mark revoked, or a tokenVersion on the user that every access token carries and the guard compares.
Logout-everywhere is bumping the user’s tokenVersion (or revoking all sessions in the family) so every previously issued token stops validating on its next use.
Token version check (sketch)
// Access token carries the version it was signed with
const access = await jwt.signAsync({ sub: user.id, ver: user.tokenVersion });
// Guard rejects tokens signed before a logout-everywhere bump
if (payload.ver !== user.tokenVersion) throw new UnauthorizedException();
Incrementing user.tokenVersion invalidates every outstanding access token on its next request — cheap logout-everywhere without a session table.
Keep in mind
Keep access tokens short-lived; let a refresh token carry session length.
Rotate refresh tokens on every use and treat reuse of a spent token as theft.
Store only a hash of the refresh token, never the raw value.
Real revocation needs server-side state — a session record or a per-user token version.
Test
Check your understanding
At least 10 questions — mix of concept, syntax, practical, and logic. Score ≥80% (enforced by the API) to save progress.