Episode ini menutup alur enrollment: pengguna memasukkan 6 digit setelah memindai QR, server memverifikasinya dengan authenticator check dan toleransi window, baru mengaktifkan totpEnabled. Kalian juga melihat kapan recovery codes pertama kali ditampilkan.

Halaman Enable 2FA sudah menampilkan QR, dan pengguna sudah memindainya. Tapi 2FA belum aktif — kode pertama belum diverifikasi. Episode 6 menutup alur enrollment dengan verifikasi code pertama: pengguna mengetik 6 digit yang tampil di Google Authenticator, dan server memastikan kode itu benar-benar cocok dengan secret yang dienkripsi di URI.
Mengapa langkah ini tidak boleh dilewati? Karena verifikasi pertama membuktikan tiga hal sekaligus: QR terbaca benar, secret sampai utuh ke aplikasi pengguna, dan jam kedua sisi cukup sinkron. Setelah itu baru aman mengaktifkan totpEnabled dan menampilkan recovery codes.
Setelah memindai, pengguna membuka Google Authenticator, melihat 6 digit, lalu memasukkannya ke formulir konfirmasi. Server menerima kode dan membandingkannya dengan secret yang disimpan sementara:
const { authenticator } = require('otplib');
app.post('/account/enable-2fa/confirm', (req, res) => {
const { token, secret } = req.body;
const valid = authenticator.check(token, secret);
if (!valid) {
return res.status(400).json({ error: 'Kode tidak valid' });
}
encryptAndSaveSecret(req.user.id, secret);
enableTotp(req.user.id);
res.json({ ok: true });
});Metode authenticator.check mengembalikan boolean: true jika kode cocok dengan secret dalam jendela waktu yang diizinkan, false jika salah. Setelah true, secret dienkripsi dan disimpan permanen, lalu totpEnabled diaktifkan.
Kedua operasi itu — menyimpan secret dan mengaktifkan flag — idealnya dibungkus dalam satu transaksi database agar tidak ada state di mana secret tersimpan tapi 2FA belum aktif.
Kode TOTP hanya valid dalam jendela 30 detik. Karena jam server dan perangkat bisa berbeda beberapa detik, otplib memberi toleransi melalui opsi window. Nilai 1 berarti server menerima kode untuk step sekarang, satu step sebelumnya, dan satu step sesudahnya:
[step-1] [step saat ini] [step+1]
30dtk 30dtk 30dtk <- masing-masing 6 digitDefault otplib adalah window 0. Atur ke 1 di produksi untuk menyerap selisih jam kecil tanpa mengorbankan keamanan: window: 1. Kalau kode kadang valid kadang tidak, jangan menaikkan window tanpa batas — periksa sinkronisasi jam lewat NTP seperti episode 2.
Istilah step merujuk pada satu jendela 30 detik. Step saat ini, step sebelumnya, dan step sesudahnya membentuk tiga kandidat kode yang dianggap valid — otplib membandingkan semuanya saat window bernilai 1. Nilai window yang lebih besar dari 2 hampir tidak pernah dibenarkan karena berarti server menerima kode yang sudah basi puluhan detik.
Warning
Setiap kegagalan verifikasi sebaiknya dicatat. Tiga sampai lima percobaan gagal berturut-turut pada langkah MFA umumnya menandakan serangan brute-force atau pengguna yang kehilangan perangkat — aktifkan cooldown atau minta verifikasi ulang. Mekanisme rate limiting dibahas penuh di episode 10.
Kunci keamanan alur ini: totpEnabled baru berubah menjadi true di dalam handler yang sama setelah authenticator.check mengembalikan true. Jangan pernah menyediakan endpoint terpisah yang mengaktifkan 2FA tanpa verifikasi — itu membuka celah pengguna (atau penyerang dengan session) mengaktifkan 2FA ke perangkatnya sendiri tanpa bukti kepemilikan secret.
Setelah aktivasi sukses, inilah saat yang tepat menampilkan recovery codes untuk pertama kali. Kode pemulihan harus ditampilkan sekali dan diminta pengguna menyimpannya sebelum bisa lanjut — karena setelah 2FA aktif, kehilangan authenticator tanpa recovery code berarti kunci keluar akun. Pembuatan dan pengelolaan recovery codes dibedah penuh di episode 9.
Kode 6 digit ruang pencariannya kecil — hanya satu juta kemungkinan — sehingga kegagalan berulang harus direspons dengan cooldown. Strategi umum: batasi percobaan per menit, lalu tambahkan delay eksponensial setelah beberapa kegagalan. Kalau pengguna sama sekali tidak bisa masuk, arahkan ke alur recovery codes yang dibahas di episode 9.
Cooldown yang baik juga membedakan pencatatan per akun dan per IP, sehingga serangan yang terdistribusi tetap terlihat sebagai pola dari satu sudut pandang.
Pesan error tidak boleh membocorkan status akun. "Kode tidak valid" lebih aman daripada pesan yang menyebut 2FA, karena penyerang tidak bisa membedakan apakah sebuah akun mengaktifkan 2FA atau tidak. Berikan hint tentang sinkronisasi jam hanya pada halaman yang memang memerlukan kode TOTP.
Sembunyikan juga detail window yang dipakai — penyerang tidak perlu tahu berapa banyak kode basi yang masih diterima server.
Secret yang disimpan sementara saat enrollment juga butuh batas hidup. Jika pengguna tidak pernah menyelesaikan verifikasi pertama dalam waktu tertentu, hapus secret pending dan tampilkan halaman enrollment baru. Ini menjaga kebersihan database dan mencegah QR basi tetap berlaku selamanya.
Penghapusan juga harus mencabut semua token pending yang sudah dikeluarkan untuk sesi enrollment tersebut, supaya halaman yang masih terbuka di browser tidak bisa menyelesaikan verifikasi dengan secret usang.
Supaya jelas, berikut situasi yang membuat verifikasi pertama gagal:
Untuk tiga kasus terakhir, solusi yang tepat adalah memulai ulang enrollment dengan secret baru, bukan menaikkan toleransi window. Secret baru dianggap sebagai rotasi dan diizinkan selama 2FA belum aktif.
Sebaliknya, kegagalan karena kode salah ketik cukup dibiarkan tanpa efek samping — pengguna tinggal mencoba lagi pada step berikutnya.
Episode 6 menutup alur enrollment dengan verifikasi pertama: pengguna mengetik 6 digit, server memeriksa dengan authenticator.check dan window toleransi, baru mengaktifkan totpEnabled dan menampilkan recovery codes.
Inti yang harus dibawa pulang:
Di episode 7 selanjutnya kita akan membahas menyimpan secret dengan aman — mengenkripsi secret Base32 dengan AES-256-GCM, mengelola kunci lewat environment MFA_ENCRYPTION_KEY, dan praktik logging yang tidak pernah membocorkan secret atau kode.