Belajar 2FA Authentication - Backup & Recovery Codes
Episode 9 of 23

Belajar 2FA Authentication - Backup & Recovery Codes

Episode ini membahas recovery codes sebagai jalan keluar saat authenticator hilang: membuat 10 kode single-use, menampilkannya sekali, menyimpannya sebagai hash seperti password, menandai kode terpakai, dan mengganti seluruh batch saat regenerasi.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Sebuah fakta yang sering dilupakan perancang fitur: perangkat pengguna bisa hilang, rusak, atau terhapus aplikasinya. Tanpa jalan keluar, pengguna terkunci di luar akunnya sendiri — dan tiket support mulai membanjir. Episode 9 membahas recovery codes, kunci darurat yang membuka pintu saat authenticator tidak tersedia.

Recovery codes harus diperlakukan serumius password: dibuat acak, ditampilkan sekali, disimpan sebagai hash, dan hanya valid satu kali. Kalian akan membangun seluruh siklus hidupnya — pembuatan, tampilan, verifikasi, sampai regenerasi — sehingga fitur ini tidak menjadi celah keamanan baru.

Membuat Recovery Codes

Sepuluh Kode Single-Use

Praktik standar adalah membuat 10 kode single-use dengan panjang 8-10 karakter. Karena kode dipakai satu kali, batch 10 cukup untuk sekitar 10 kali pemulihan sebelum regenerasi. Buat dengan kriptografi acak, bukan Math.random:

Generate 10 recovery code acak
node -e "
const crypto = require('crypto');
for (let i = 0; i < 10; i++) {
  console.log(crypto.randomBytes(6).toString('hex').toUpperCase());
}
"

Setiap baris output adalah satu kode 12 karakter hex. Panjang minimal 8 karakter memberi ruang pencarian yang cukup besar, sementara format tanpa karakter ambigu membuat kode mudah diketik saat darurat.

Tampilkan Sekali

Kode hanya boleh ditampilkan sekali, pada layar yang meminta pengguna menyalin dan menyimpannya sebelum lanjut. Simpan kode sebagai string yang bisa disalin, dan beri peringatan tegas bahwa layar ini tidak akan muncul lagi. Setelah layar ditutup, server tidak lagi menyimpan plaintext kode — hanya hash-nya.

Menyimpan dengan Aman

Hash Sebelum Disimpan

Sama seperti password, recovery code tidak boleh disimpan plaintext. Hash dengan bcrypt atau argon2 sebelum masuk database. Ini memastikan bahwa jika database bocor, kode tidak bisa langsung dipakai:

JSHash recovery code sebelum disimpan
const { recoveryCodes } = require('../db');
 
async function storeRecoveryCodes(userId, codes) {
  for (const code of codes) {
    const codeHash = await bcrypt.hash(code, 10);
    await recoveryCodes.create({ data: { userId, codeHash } });
  }
}

recoveryCodes.create menyimpan satu baris per kode dengan kolom codeHash dan status belum terpakai. Hashing per kode memperlambat brute-force offline jika tabel bocor.

Satu Baris per Kode

Struktur tabel memakai satu baris per kode, bukan satu string panjang, agar penandaan terpakai dan penolakan kode yang sudah dipakai mudah dilakukan. Episode 12 akan membahas skema lengkapnya.

Verifikasi dan Penandaan Terpakai

Alur Masuk Darurat

Saat pengguna memilih opsi "gunakan recovery code", server mencari kode yang cocok di antara kode milik pengguna yang belum terpakai:

JSVerifikasi dan tandai recovery code terpakai
const matches = await recoveryCodes.findMany({ where: { userId } });
for (const row of matches) {
  if (await bcrypt.compare(code, row.codeHash) && !row.usedAt) {
    await recoveryCodes.update({
      where: { id: row.id },
      data: { usedAt: new Date() }
    });
    return res.json({ ok: true });
  }
}
res.status(400).json({ error: 'Kode tidak valid' });

Dua syarat wajib pada baris yang cocok: hash cocok dan usedAt masih kosong. Setelah kode dipakai, usedAt diisi sehingga kode yang sama ditolak pada percobaan berikutnya — ini sifat single-use yang harus dijaga ketat.

Sesudah Masuk dengan Recovery Code

Penting untuk menandai bahwa pengguna masuk lewat recovery code. Banyak sistem menampilkan peringatan: perangkat authenticator tidak terkonfirmasi, dan disarankan mengaktifkan ulang 2FA atau regenerasi kode. Pengguna yang masuk tanpa perangkatnya harus diarahkan untuk mengamankan akun lagi secepat mungkin.

Regenerasi Batch

Invalidate Batch Lama

Ketika pengguna meminta kode baru, seluruh batch lama harus di-invalidate. Jika tidak, kode lama yang mungkin sudah tercuri tetap berlaku:

JSRegenerasi batch recovery code
await recoveryCodes.deleteMany({ where: { userId } });
const freshCodes = generateCodes(10);
await storeRecoveryCodes(userId, freshCodes);

recoveryCodes.deleteMany menghapus semua kode lama sebelum batch baru disimpan. Operasi ini sebaiknya dibungkus transaksi, dan hanya boleh dijalankan setelah pengguna membuktikan identitasnya — misalnya lewat password dan kode TOTP saat ini.

UX Penyimpanan yang Aman

Saat menampilkan kode, beri saran penyimpanan yang konkret: password manager, aplikasi catatan terenkripsi, atau kertas yang disimpan di tempat aman. Jangan pernah menawarkan penyimpanan otomatis di browser sebagai satu-satunya pilihan, dan peringatkan pengguna untuk tidak menyimpan kode di screenshot yang ikut ter-backup ke cloud publik.

Penutup

Episode 9 membangun recovery codes dari nol: 10 kode single-use yang dibuat acak, ditampilkan sekali, disimpan sebagai hash, diverifikasi sekali pakai, dan bisa diganti seluruh batch dengan sekali klik.

Inti yang harus dibawa pulang:

  • Buat 10 kode single-use dengan panjang 8-10 karakter.
  • Tampilkan kode hanya sekali dan minta pengguna menyimpannya.
  • Hash recovery code dengan bcrypt atau argon2 sebelum disimpan.
  • Tandai usedAt saat kode dipakai untuk menegakkan single-use.
  • Waspadai masuk lewat recovery code dan arahkan pengguna mengamankan akun.
  • Regenerasi menghapus batch lama dalam satu transaksi.

Di episode 10 selanjutnya kita akan membahas rate limiting dan replay protection — membatasi percobaan kode dengan express-rate-limit, dan melacak time-step terakhir agar kode yang sama tidak bisa dipakai ulang dalam jendela 30 detik.