Episode ini memperkuat endpoint verifikasi: rate limiting dengan express-rate-limit untuk mempersempit brute-force 6 digit, dan replay protection dengan melacak time-step terakhir agar kode TOTP tidak bisa dipakai ulang dalam jendela 30 detik yang sama.

Kode TOTP 6 digit memiliki ruang pencarian hanya satu juta kemungkinan. Tanpa pengaman tambahan, penyerang bisa menebak kode dengan mencoba ribuan kali per detik. Episode 10 menutup dua celah yang saling melengkapi: rate limiting untuk memperlambat percobaan, dan replay protection agar kode yang sudah sah tidak dipakai ulang.
Keduanya berbeda tapi saling menopang. Rate limiting menyerang bagian serangan, replay protection menyerang kelemahan protokol. Kalian akan memasang keduanya pada endpoint verifikasi MFA, dan memahami cara kerjanya di balik layar.
express-rate-limit@7 dipasang sebagai middleware pada route yang paling sensitif: verifikasi kode. Batasan umumnya 5 percobaan per menit per IP atau per akun:
const { rateLimit } = require('express-rate-limit');
const mfaLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 5,
standardHeaders: 'draft-7',
legacyHeaders: false
});
app.post('/login/mfa', mfaLimiter, mfaHandler);
app.post('/account/enable-2fa/confirm', mfaLimiter, confirmHandler);Middleware mfaLimiter menghitung permintaan per IP dalam jendela 60 detik. Setelah 5 percobaan, permintaan berikutnya ditolak dengan status 429 sampai jendela bergeser. Opsi standardHeaders mengirim header RateLimit yang bisa dibaca client.
Rate limit berbasis IP mencegah penyerang tunggal, tapi bisa menjebak pengguna di belakang NAT bersama. Rate limit berbasis akun lebih adil tapi bisa dilewati penyerang dengan banyak akun. Solusi terbaik adalah menggabungkan keduanya: limit ketat per akun dan limit longgar per IP sebagai jaring pengaman.
Jangan lupa melindungi endpoint konfirmasi enrollment dengan limit yang sama. Secret 6 digit yang diketik saat aktivasi sama rapuhnya dengan kode login, dan penyerang dengan akses ke secret pending bisa menebak kode untuk membajak enrollment.
Sebagai aturan praktis, semua route yang memproses 6 digit TOTP harus memakai limit yang sama ketatnya dengan /login/mfa — konsistensi ini lebih mudah diuji daripada daftar pengecualian yang panjang.
Window toleransi yang diizinkan otplib (misalnya window 1) membuat kode tetap valid selama 90 detik. Dalam rentang itu, kode yang sama bisa dipakai ulang jika server tidak melacak pemakaian — misalnya penyerang yang menyadap satu kode memakainya sebelum pemilik asli. RFC 6238 menyebut kontrol ini dengan istilah mitigasi replay.
Solusinya sederhana: simpan time-step terakhir yang berhasil, dan tolak kode yang berasal dari step yang sudah tercatat:
const currentStep = Math.floor(Date.now() / 1000 / 30);
if (user.lastUsedStep === currentStep) {
return res.status(400).json({ error: 'Kode sudah dipakai' });
}
const valid = authenticator.check(req.body.token, decryptSecret(user.totp_secret_encrypted));
if (!valid) {
return res.status(400).json({ error: 'Kode tidak valid' });
}
await users.update({
where: { id: user.id },
data: { lastUsedStep: currentStep }
});Kolom lastUsedStep menyimpan nilai step yang terakhir berhasil. Jika percobaan berikutnya datang dengan step yang sama, server langsung menolak sebelum menghitung kode — murah dan efektif. Update dilakukan hanya setelah kode benar.
Visualisasi tiga step dalam satu jendela:
step-1 step step+1
15 dtk 30 dtk 30 dtk <- rentang kode valid
x (sudah dipakai) x (dipakai sekarang)
terakhir dipakai = step <- step yang sama ditolakKode dari step yang sama ditolak meski masih dalam jendela toleransi — inilah yang membedakan toleransi waktu dari izin pemakaian ganda.
Limit di bawah 10 percobaan per menit untuk 6 digit sering dianggap cukup, tapi sebenarnya masih memberi penyerang puluhan ribu percobaan per jam dengan banyak IP. Tetapkan limit ketat di layer aplikasi, lalu perketat lagi di layer edge seperti CDN atau firewall.
Mengupdate lastUsedStep sebelum memeriksa kode akan memblokir pengguna sah yang mengetik kode salah pada step yang sama. Update harus selalu berjalan setelah verifikasi berhasil — urutan di kode contoh di atas bukan kebetulan.
Episode 10 menutup dua celah pada endpoint verifikasi: rate limiting memperlambat brute-force 6 digit dengan express-rate-limit, dan replay protection menolak kode yang sudah dipakai melalui pencatatan lastUsedStep.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita akan membahas disable, reset, dan rotasi secret — mematikan 2FA dengan re-autentikasi, mengganti secret saat perangkat dicurigai bocor, dan siklus hidup status pengguna dari disabled ke pending lalu enabled.