Access and Refresh Tokens

Access Tokens, Refresh Tokens and Logout

The fix for an irrevocable token is to make it short-lived and pair it with a long-lived refresh token that is opaque, stored server-side, and therefore deletable. The access token buys speed, the refresh token buys control.

Refresh token rotation, and what reuse detection does
Refresh token rotation, and what reuse detection does
Rotating refresh tokens with reuse detectionJavaScript
const RT_COOKIE = { httpOnly: true, secure: true, sameSite: 'strict',
                    path: '/auth/refresh', maxAge: 30 * 864e5 };
app.post('/auth/refresh', async (req, res) => {
  const rec = await tokens.findByHash(sha256(req.cookies.rt ?? ''));
  if (!rec || rec.expiresAt < Date.now()) {
    return res.status(401).json({ error: 'invalid_refresh_token' });
  }
  if (rec.usedAt) {                          // a replay: the family is compromised
    await tokens.revokeFamily(rec.familyId);
    return res.status(401).json({ error: 'token_reuse_detected' });
  }
  await tokens.markUsed(rec.id);
  const user = await users.byId(rec.userId);
  res.cookie('rt', await tokens.issue(user, rec.familyId), RT_COOKIE)
     .json({ access_token: issueAccess(user), expires_in: 900 });
});

Store a hash of the refresh token, not the token: that table is a list of live credentials, and Hashes and HMACs's argument for hashing applies unchanged. Scoping the cookie to path: '/auth/refresh' keeps the browser from attaching it to ordinary API calls. As for the access token in a browser, localStorage is readable by any script on the page, so one XSS gives it away; keep both tokens in httpOnly cookies and pay the CSRF price (CSRF Protection After csurf). Native clients are where a header-carried token is simpler.

Logout has three parts, and skipping one leaves a working credential behind: delete the refresh record server-side, clear the cookies, and accept that outstanding access tokens stay valid until they expire. If minutes are too long, keep a deny-list of jti values in Redis 2,763 with a TTL equal to the access token lifetime.