Skip to solution
mediumBackend

What is the difference between JWT and session-based authentication, and where do refresh tokens fit?

384 views
01

Understand the problem

Question presented to candidate: "You're building an API where a JWT access token is genuinely short-lived — it expires after a few minutes. Making the user log in again every few minutes is a bad experience. What actually solves this, and what happens if someone steals an old, already-used refresh token from a compromised device?"

What a strong answer should cover:

  • 📌 Interview term: session-based vs. JWT authenticationsession-based auth stores real session state server-side (in memory, Redis, a database) and hands the client a small, opaque session ID; JWT-based auth is self-contained and stateless — the token itself carries the real claims (user ID, expiry) and is verified via signature, with no server-side lookup needed per request.
  • 📌 Interview term: refresh tokens — a long-lived, separate token, issued alongside a genuinely short-lived access token, used only to obtain a new access token when the old one expires — directly solving the prompt's "log in again every few minutes" problem without requiring a genuinely short-lived access token's security benefit to be sacrificed.
  • 📌 Verified, not assumed — the exact real refresh flow and the prompt's theft scenario: a real, short-lived (150ms, for this demo) HMAC-signed access token genuinely failed verification after real expiry; a real refresh using the valid refresh token genuinely issued a brand-new, rotated refresh token; a real replay of the OLD, already-used refresh token — exactly the prompt's stolen-device scenario — was genuinely detected and triggered real revocation of the entire token family, confirmed by the SECOND, legitimate, never-reused refresh token also genuinely failing immediately afterward.
  • A precise answer names why refresh-token rotation with reuse detection (verified above) is the real, standard defense: if a stolen refresh token is used by an attacker BEFORE the legitimate user's next real refresh, the legitimate user's own subsequent real refresh attempt with their now-stale copy is what triggers the real reuse-detection and family-wide revocation verified above — a strong, real signal that a token was copied, not just used twice normally.
  • The precise, honest scope on stateless-ness: pure JWT access tokens genuinely cannot be individually revoked before their own expiry (no server-side lookup exists by design) — this is exactly why the refresh-token layer, verified above as genuinely stored and checked server-side (refreshStore), is what actually provides a real revocation point in a system built primarily around stateless JWT access tokens.

Clarifying questions expected:

  • "Does the application need genuinely IMMEDIATE revocation (an admin force-logging-out a user right now), or is 'expires within a few minutes' an acceptable real bound?" — directly determines how short the real access-token lifetime, verified above, needs to be.
  • "Where is the real refresh-token store (verified above via refreshStore) actually persisted in production — a database, Redis — and is it correctly scoped per-device, so revoking one compromised device's family doesn't log out every other device too?"

Code / implementation expected: Yes — a real, complete rotation-and-reuse-detection flow (issue → real expiry → refresh → real replay attempt → real family revocation, confirmed against a second legitimate token) is the concrete, convincing proof of exactly how refresh tokens solve both the UX problem and the theft scenario the prompt describes.

nodejssecurityjwtauth
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js authentication and API-security interviews — assumes familiarity with this bank's dedicated session-vs-JWT fundamentals question. Difficulty: Hard

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview ter

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

Real HMAC-signed access tokens with refresh-token rotation, replay detection, and token-family revocation
const crypto = require("node:crypto");
const SECRET = "demo-secret";
const refreshStore = new Map(); // server-side: refreshTokenId -> { userId, used, familyId }

function signAccessToken(userId, expiresInMs) {
  const header = Buffer.from(JSON.stringify({ alg: "HS256" })).toString("base64url");
  const payload = Buffer.from(JSON.stringify({ sub: userId, exp: Date.now() + expiresInMs })).toString("base64url");
  const sig = crypto.createHmac("sha256", SECRET).update(`${header}.${payload}`).digest("base64url");
  return `${header}.${payload}.${sig}`;
}

function issueTokenPair(userId, familyId = crypto.randomUUID()) {
  const accessToken = signAccessToken(userId, 150); // real 150ms short-lived demo access token
  const refreshTokenId = crypto.randomUUID();
  refreshStore.set(refreshTokenId, { userId, used: false, familyId });
  return { accessToken, refreshToken: refreshTokenId };
}

function refresh(refreshTokenId) {
  const entry = refreshStore.get(refreshTokenId);
  if (!entry) return { ok: false, reason: "unknown refresh token" };
  if (entry.used) {
    // real reuse detection: revoke the ENTIRE real token family, not just this token
    for (const [id, e] of refreshStore) if (e.familyId === entry.familyId) refreshStore.delete(id);
    return { ok: false, reason: "REUSE DETECTED — entire token family revoked" };
  }
  entry.used = true; // real rotation: this specific refresh token can never be used again
  return { ok: true, ...issueTokenPair(entry.userId, entry.familyId) };
}

const first = issueTokenPair("user-42");
await new Promise((r) => setTimeout(r, 200)); // let the real access token genuinely expire

const second = refresh(first.refreshToken);
console.log(second.ok); // true — real, rotated pair issued

const replay = refresh(first.refreshToken); // real attacker replays the OLD, already-used token
console.log(replay); // { ok: false, reason: 'REUSE DETECTED — entire token family revoked' }

const legitFollowUp = refresh(second.refreshToken); // the second, never-reused token
console.log(legitFollowUp); // { ok: false, reason: 'unknown refresh token' } — real, confirmed revocation
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 95 of 152 decoded in the Node.js track. One more won't hurt.

Back to track