Refresh Token Rotation — Auth

Refresh Token Rotation adalah strategi keamanan di mana setiap kali refresh token digunakan, token tersebut langsung diinvalidasi dan digantikan dengan pasangan

Refresh Token Rotation adalah strategi keamanan di mana setiap kali refresh token digunakan, token tersebut langsung diinvalidasi dan digantikan dengan pasangan token baru. Ini mencegah serangan token replay.

Masalah tanpa rotasi:

Jika refresh token dicuri, penyerang bisa terus-menerus menggunakannya untuk mendapatkan access token baru — tanpa diketahui user yang sah.

Alur Refresh Token Rotation:

1. Login → Server beri: accessToken (15 menit) + refreshToken_v1 (30 hari)
2. Access token expired → client kirim refreshToken_v1
3. Server validasi refreshToken_v1:
   - Tandai refreshToken_v1 sebagai USED/INVALID
   - Buat accessToken baru + refreshToken_v2
   - Kirim kedua token baru ke client
4. Jika refreshToken_v1 dipakai LAGI (reuse attack):
   - Server deteksi token sudah dipakai → REVOKE SEMUA token di family
   - User harus login ulang → penyerang terblokir

Implementasi:

const tokenStore = new Map(); // { tokenId: { userId, familyId, used, expiresAt } }

function generateTokenPair(userId, familyId = crypto.randomUUID()) {
  const refreshTokenId = crypto.randomUUID();
  const accessToken = jwt.sign({ userId, type: "access" }, SECRET, { expiresIn: "15m" });
  const refreshToken = jwt.sign({ tokenId: refreshTokenId, familyId, type: "refresh" }, SECRET, { expiresIn: "30d" });

  tokenStore.set(refreshTokenId, {
    userId,
    familyId,
    used: false,
    expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000
  });

  return { accessToken, refreshToken };
}

async function rotateTokens(refreshToken) {
  const payload = jwt.verify(refreshToken, SECRET);
  const { tokenId, familyId } = payload;
  const stored = tokenStore.get(tokenId);

  if (!stored) throw new Error("Token tidak ditemukan");
  if (stored.used) {
    // Reuse attack detected! Revoke entire family
    revokeFamily(familyId);
    throw new Error("Token sudah dipakai — kemungkinan serangan! Semua sesi diakhiri.");
  }
  if (Date.now() > stored.expiresAt) throw new Error("Token kadaluarsa");

  // Invalidate old token
  stored.used = true;

  // Issue new pair (same family)
  return generateTokenPair(stored.userId, familyId);
}

function revokeFamily(familyId) {
  for (const [id, token] of tokenStore.entries()) {
    if (token.familyId === familyId) {
      tokenStore.delete(id);
    }
  }
}

Deteksi Reuse Attack:

Kuncinya adalah menyimpan status token di server (database/Redis). Ketika token yang sudah used: true dipakai lagi, itu sinyal bahwa token dicuri — langsung revoke seluruh "family" token.

Tips implementasi: