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:
- Simpan refresh token di HttpOnly cookie (bukan localStorage)
- Beri setiap token family ID unik untuk revokasi massal
- Tambahkan binding ke IP/user-agent untuk deteksi anomali
- Bersihkan token kadaluarsa secara berkala (cron job)
- Log semua event rotasi untuk audit