Episode ini menguji 2FA secara menyeluruh: validasi TOTP terhadap test vector resmi RFC 6238 dengan fake timers, uji clock skew dan window toleransi, serta E2E dengan Google Authenticator sungguhan termasuk skenario negatif kode salah, kedaluwarsa, dan replay.

Fitur 2FA mengatur kriptografi dan waktu — dua hal yang paling sulit diuji secara manual. Episode 16 membahas strategi pengujian yang menyeluruh: unit test dengan test vector resmi RFC 6238, pengujian waktu dengan fake timers, dan E2E dengan Google Authenticator sungguhan.
Mengapa pengujian ini tidak bisa ditunda? Karena bug TOTP muncul paling buruk di produksi — kode yang kadang diterima, kadang tidak, membuat pengguna terkunci dan tiket support melonjak. Kalian akan membangun tiga lapis pengujian yang menangkap masalah ini jauh sebelum pengguna merasakannya.
Tiga lapis itu saling melengkapi, bukan saling menggantikan: unit test membuktikan rumusnya benar, uji waktu membuktikan toleransinya masuk akal, dan E2E membuktikan pengguna nyata bisa melewati alur.
RFC 6238 memuat tabel test vector: pasangan waktu dan kode yang dijamin benar untuk secret tertentu. Vektor ini adalah tolok ukur — jika library menghasilkan kode yang sama, implementasinya sesuai standar. Secret yang dipakai adalah ASCII "12345678901234567890" yang di-encode Base32.
Uji dengan fake timers dan vektor SHA1 8 digit:
const { authenticator } = require('otplib');
const SECRET = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ';
jest.useFakeTimers();
jest.setSystemTime(new Date('1970-01-01T00:00:59.000Z'));
test('TOTP cocok dengan vektor RFC 6238', () => {
authenticator.options = { algorithm: 'SHA1', digits: 8, step: 30 };
expect(authenticator.generate(SECRET)).toBe('94287082');
});Waktu 1970-01-01 00:00:59 menghasilkan counter 1, dan vektor RFC 6238 untuk counter 1 dengan SHA1 dan 8 digit adalah 94287082. jest.setSystemTime menggantikan jam sungguhan sehingga pengujian tidak bergantung pada waktu yang sedang berjalan.
Untuk kenyamanan, tetapkan secret ini sebagai konstanta di helper test, sehingga semua file pengujian memakai sumber yang sama. Jika secret berubah di satu tempat tanpa mengubah yang lain, kegagalan test langsung menunjukkan inkonsistensi.
Satu vektor mengonfirmasi dasar, bukan keseluruhan. Uji beberapa titik: counter 0, counter bernilai besar, digits 6 dan 8, serta algoritma SHA256 dan SHA512 yang juga punya vektor di RFC. Setiap kombinasi menutup cabang kode yang berbeda di dalam library dan memastikan opsi konfigurasi diterjemahkan dengan benar.
Gunakan juga uji round-trip sederhana: generate dengan otplib lalu verify dengan check pada waktu yang sama, untuk memastikan pasangan generate dan verify berjalan konsisten di luar vektor statis.
TOTP hidup dan mati di sinkronisasi jam. Uji toleransi dengan memajukan waktu melewati batas step:
authenticator.options = { step: 30, window: 1 };
jest.setSystemTime(new Date('1970-01-01T00:00:00.000Z'));
const kodeAwal = authenticator.generate(SECRET);
jest.setSystemTime(new Date('1970-01-01T00:00:45.000Z'));
const kodeBerikutnya = authenticator.generate(SECRET);
expect(authenticator.check(kodeAwal, SECRET)).toBe(true);
expect(authenticator.check(kodeBerikutnya, SECRET)).toBe(true);Pada detik ke-45, step saat ini adalah 1 sedangkan kodeAwal berasal dari step 0. Dengan window 1, kode lama tetap diterima — persis perilaku yang membuat pengguna dengan jam sedikit terlambat tidak terkunci.
Uji juga sisi gagalnya: kode dari step yang jauh di luar window harus ditolak, dan kode yang sama tidak boleh diterima dua kali (replay). Dua skenario ini adalah yang paling sering lolos dari pengujian manual dan paling berbahaya di produksi — episode 10 membangun mekanisme lastUsedStep yang wajib diuji di sini.
Untuk simulasi jam yang menyimpang, beberapa framework menyediakan helper untuk memajukan waktu sistem di proses test — gunakan dengan hati-hati dan kembalikan jam sebenarnya setelah selesai agar test lain tidak terganggu.
Unit test membuktikan matematika; E2E membuktikan kompatibilitas. Jalankan aplikasi di HTTPS lokal (episode 13), buka halaman Enable 2FA, dan pindai QR dengan Google Authenticator sungguhan:
1. Pindai QR dengan Google Authenticator
2. Verifikasi kode pertama di form konfirmasi
3. Logout lalu login ulang
4. Input kode TOTP dari aplikasi
5. Uji recovery code setelah menghapus akun dari aplikasiPengujian dengan perangkat nyata menangkap hal yang tidak terlihat di unit test: kompatibilitas pemindaian, format secret yang ditampilkan, dan perilaku aplikasi authenticator pada versi tertentu.
Sebaiknya lakukan uji ini di beberapa perangkat: Android dan iOS memakai versi Google Authenticator yang berbeda, dan perilaku import/export bisa berbeda antar platform.
Lengkapi dengan skenario negatif: kode salah ketik ditolak, kode kedaluwarsa ditolak, kode yang sama dipakai dua kali ditolak, recovery code terpakai ditolak, dan rate limit memblokir setelah 5 percobaan. Catat hasilnya dalam daftar periksa yang bisa diulang di setiap rilis — ini melindungi dari regresi yang diam-diam masuk bersama perubahan lain.
Otomatiskan skenario ini bila memungkinkan lewat integrasi test, dan simpan output manual sebagai lampiran rilis — bukti uji sering diminta dalam audit.
Episode 16 menutup pengujian dengan tiga lapis: unit test terhadap vektor RFC 6238 dengan fake timers, pengujian clock skew dan window toleransi, serta E2E dengan Google Authenticator sungguhan termasuk semua skenario negatif.
Inti yang harus dibawa pulang:
Di episode 17 selanjutnya kita akan membahas melampaui TOTP: WebAuthn dan passkeys — prinsip FIDO2 yang tahan phishing tanpa shared secret, perbandingan TOTP versus WebAuthn versus SMS, dan strategi menawarkan keduanya sekaligus.