Script Valley
REST API Development: Beginner to Production
Authentication and AuthorizationLesson 4.3

Refresh tokens — implementing secure token rotation

access token vs refresh token, token rotation, httpOnly cookies, refresh endpoint, token families, revocation, sliding sessions

Access Tokens and Refresh Tokens

Short-lived access tokens minimize damage from token theft. Refresh tokens extend sessions without requiring re-login, but require server-side storage for revocation.

Issuing Both Tokens at Login

const issueTokens = async (res, user) => {
  const accessToken = jwt.sign(
    { sub: user.id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  const refreshToken = jwt.sign(
    { sub: user.id },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' }
  );

  // Store refresh token in DB for revocation
  await RefreshToken.create({ token: refreshToken, userId: user.id });

  // Send refresh token in httpOnly cookie — not accessible by JavaScript
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'Strict',
    maxAge: 7 * 24 * 60 * 60 * 1000
  });

  return accessToken; // Send access token in body
};

Refresh Endpoint

app.post('/auth/refresh', async (req, res) => {
  const token = req.cookies.refreshToken;
  if (!token) return res.status(401).json({ error: 'No refresh token' });

  const stored = await RefreshToken.findOne({ token });
  if (!stored) return res.status(403).json({ error: 'Token reuse detected' });

  await stored.delete(); // Rotate — invalidate old token
  const decoded = jwt.verify(token, process.env.REFRESH_SECRET);
  const user = await User.findById(decoded.sub);
  const accessToken = await issueTokens(res, user); // Issue new pair
  res.json({ accessToken });
});

Up next

Role-based access control (RBAC) in Express APIs

Sign in to track progress