Episode ini membahas pembuatan secret TOTP yang acak dan unik per pengguna dengan otplib, menyusun provisioning URI otpauth yang dikenali Google Authenticator, encoding issuer dan account, serta alasan Base32 RFC 3548 menjadi bahasa bersama seluruh authenticator app.

Semua dimulai dari satu rahasia: shared secret yang akan dipakai server dan Google Authenticator untuk menghitung kode yang sama. Episode 4 membahas cara membuat secret itu dengan benar, lalu menyusunnya menjadi provisioning URI berformat otpauth:// yang dipahami semua authenticator app.
Kesalahan di tahap ini berakibat fatal — secret yang lemah berarti kode bisa ditebak, secret yang tidak unik berarti satu perangkat bisa membuka banyak akun. Di akhir episode kalian akan memiliki fungsi siap pakai untuk membuat secret dan URI yang langsung bisa dipindai.
otplib menyediakan authenticator.generateSecret() yang memanfaatkan kriptografi acak dari Node untuk membuat secret Base32 20 karakter (160 bit) — standar yang direkomendasikan RFC 4226. Secret ini mewakili 20 byte entropy, jauh di atas kebutuhan minimum.
Secret 160 bit dari otplib memang lebih panjang dari minimum 128 bit yang disarankan sebagian pedoman — sengaja, untuk memberi ruang aman saat secret di-encode Base32 dan dipakai oleh HMAC-SHA1. Panjang ini juga menjadi standar de facto di sebagian besar implementasi TOTP modern.
Warning
Jangan pernah menggunakan ulang secret yang sama untuk dua akun berbeda, atau menyalin secret dari contoh tutorial ke produksi. Secret adalah kunci kepemilikan — jika satu akun bocor, secret bersama ikut membuka akun kedua.
Lihat cara kerjanya:
node -e "
const { authenticator } = require('otplib');
const secret = authenticator.generateSecret();
console.log('Secret Base32:', secret);
console.log('Panjang karakter:', secret.length);
"Jalankan dua kali dan perhatikan output berbeda setiap kali. authenticator.generateSecret() menggunakan crypto API bawaan Node, sehingga tidak bergantung pada Math.random yang tidak aman untuk kriptografi.
Secret harus dibuat per pengguna saat enrollment dimulai dan jangan pernah dipakai ulang antar akun. Jangan pernah menyalin satu secret untuk semua user — itu menghancurkan model kepemilikan "something you have". Simpan secret sementara di memori atau kolom pending hingga verifikasi pertama berhasil (episode 6), lalu baru persistenkan. Pada praktiknya, secret pending bisa disimpan sebagai kolom terpisah di tabel users yang diisi saat halaman Enable 2FA dibuka dan dihapus setelah kode pertama valid.
Agar Google Authenticator bisa memprovisioning sebuah akun, secret harus dibungkus dalam URI dengan format spesifik:
otpauth://totp/{Issuer}:{account}?secret={BASE32}&issuer={Issuer}&algorithm=SHA1&digits=6&period=30Bagian host totp menunjukkan tipe, label Issuer:account adalah nama akun yang tampil di aplikasi, sedangkan query parameter secret, issuer, algorithm, digits, dan period memberi tahu aplikasi cara menghitung kode. Nilai SHA1, 6, dan 30 adalah default Google Authenticator, tapi ditulis eksplisit agar tidak ambigu.
Ketika semua parameter ditulis eksplisit, URI yang sama akan menghasilkan kode yang identik di Google Authenticator, Microsoft Authenticator, dan Authy — karena ketiganya menghitung dari rumus RFC 6238 yang sama.
Merangkai URI secara manual rawan salah encoding. otplib menyediakan authenticator.keyuri(account, service, secret) yang mengurus semuanya:
const { authenticator } = require('otplib');
const secret = authenticator.generateSecret();
const uri = authenticator.keyuri(
'budi@example.com',
'Devvnull Labs',
secret
);
console.log(uri);Output authenticator.keyuri akan berbentuk otpauth://totp/Devvnull%20Labs:budi%40example.com?secret=.... Perhatikan bahwa spasi dan karakter khusus di-encode otomatis — misalnya %20 untuk spasi dan %40 untuk tanda at. URI inilah yang akan diubah menjadi QR code di episode 5.
Saat issuer atau account memuat karakter khusus — spasi, tanda at, titik dua — nilai tersebut harus di-encode persentase agar URI valid. Tanda titik dua dan miring hanya dipakai sebagai pemisah di bagian label, bukan di dalam nilai. Selalu lewati nilai melalui encodeURIComponent sebelum dimasukkan ke URI bila kalian merangkai manual.
Contoh encoding manual untuk issuer dan account yang mengandung spasi dan tanda at:
const issuer = encodeURIComponent('Devvnull Labs');
const account = encodeURIComponent('budi@example.com');
const label = issuer + ':' + account;
const uri = 'otpauth://totp/' + label +
'?secret=' + secret + '&issuer=' + issuer;Karena label menggunakan titik dua sebagai pemisah issuer dan account, pastikan nilai account yang mengandung tanda at sudah di-encode sebelum digabung. Keuntungan memakai authenticator.keyuri adalah encoding ini ditangani secara konsisten di satu tempat.
Google Authenticator, Microsoft Authenticator, Authy, dan 1Password semuanya memahami URI otpauth:// dengan secret Base32. Base32 didefinisikan RFC 3548 dan memakai alfabet 32 karakter: huruf A-Z dan angka 2-7. Karakter khusus seperti tanda sama dengan untuk padding dan huruf kecil bisa muncul, dan aplikasi biasanya toleran terhadap padding yang hilang.
Karena standar ini terbuka dan seragam, kalian bisa berpindah antar aplikasi authenticator tanpa kehilangan akses — cukup impor secret atau pindai ulang URI. Episode 21 akan membahas kompatibilitas lintas aplikasi ini lebih jauh.
Episode 4 mengajarkan pembuatan secret yang benar — acak, unik per pengguna, dan cukup panjang — serta penyusunan URI otpauth:// dengan otplib, termasuk encoding issuer dan account dan alasan Base32 menjadi bahasa bersama semua authenticator.
Inti yang harus dibawa pulang:
Di episode 5 selanjutnya kita akan membahas QR code dan provisioning di frontend — merender URI menjadi QR dengan qrcode, menampilkannya hanya sekali saat enrollment, dan merancang halaman Enable 2FA yang ramah pengguna.