Multi-factor Authentication (MFA) menambahkan lapisan keamanan ekstra dengan meminta lebih dari satu bentuk verifikasi identitas.
Tiga faktor autentikasi:
| Faktor | Deskripsi | Contoh |
|---|---|---|
| Something you know | Pengetahuan | Password, PIN |
| Something you have | Kepemilikan | Smartphone, hardware key |
| Something you are | Biometrik | Sidik jari, wajah, iris |
2FA (Two-Factor Authentication) — kombinasi 2 faktor. Contoh umum: password (know) + OTP dari app (have).
Metode MFA populer:
1. TOTP (Time-based One-Time Password) Standar paling umum. App seperti Google Authenticator generate kode 6 digit yang berubah setiap 30 detik.
const speakeasy = require("speakeasy");
const qrcode = require("qrcode");
// Setup MFA: generate secret
app.post("/mfa/setup", requireAuth, async (req, res) => {
const secret = speakeasy.generateSecret({
name: "BelajarWeb.dev",
issuer: "BelajarWeb"
});
// Simpan secret di database (encrypted)
await db.saveMfaSecret(req.user.id, secret.base32);
// Generate QR code untuk di-scan app authenticator
const qr = await qrcode.toDataURL(secret.otpauth_url);
res.json({ qrCode: qr, manualKey: secret.base32 });
});
// Verifikasi kode TOTP
app.post("/mfa/verify", requireAuth, async (req, res) => {
const secret = await db.getMfaSecret(req.user.id);
const valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: req.body.code, // "123456" dari user
window: 1 // toleransi 1 interval (30 detik)
});
if (valid) {
// MFA berhasil — beri akses penuh
req.session.mfaVerified = true;
res.json({ success: true });
} else {
res.status(401).json({ error: "Kode salah" });
}
});
2. SMS OTP Server mengirim kode via SMS. Lebih mudah tapi kurang aman (rentan SIM swapping).
3. WebAuthn / Passkeys Menggunakan hardware security key (YubiKey) atau biometrik device. Paling aman, tahan phishing.
// WebAuthn registration (simplified)
const options = await generateRegistrationOptions({
rpName: "BelajarWeb.dev",
rpID: "belajarweb.dev",
userID: user.id,
userName: user.email,
authenticatorSelection: {
userVerification: "preferred"
}
});
4. Backup/Recovery Codes — Deep Dive
Kode satu kali pakai untuk darurat (device hilang, authenticator app reset). Wajib disediakan saat user setup MFA — tanpa ini, user yang kehilangan phone = lockout permanen.
Generate & simpan:
import crypto from "crypto";
import bcrypt from "bcrypt";
async function setupBackupCodes(userId) {
// Generate 10 kode random, format "abcd-efgh" (8 hex char + dash untuk readability)
const plainCodes = Array.from({ length: 10 }, () => {
const hex = crypto.randomBytes(4).toString("hex");
return `${hex.slice(0, 4)}-${hex.slice(4)}`;
});
// Hash SETIAP kode (treat seperti password) — jangan simpan plain
const hashedCodes = await Promise.all(
plainCodes.map(code => bcrypt.hash(code, 10))
);
// Simpan di DB dengan status "unused"
for (const hash of hashedCodes) {
await db.query(
"INSERT INTO backup_codes (user_id, code_hash, used) VALUES ($1, $2, false)",
[userId, hash]
);
}
// Return plain codes ke UI — user harus simpan (download PDF, print, tulis)
return plainCodes;
// ⚠️ Ini satu-satunya kali plain codes ditampilkan. Jangan log, jangan email, jangan simpan di session.
}
Consumption — one-time use:
async function verifyBackupCode(userId, inputCode) {
const unused = await db.query(
"SELECT id, code_hash FROM backup_codes WHERE user_id = $1 AND used = false",
[userId]
);
for (const row of unused) {
if (await bcrypt.compare(inputCode, row.code_hash)) {
// MARK SEBAGAI USED — ini kunci one-time
await db.query(
"UPDATE backup_codes SET used = true, used_at = NOW() WHERE id = $1",
[row.id]
);
// Notify user via email/SMS bahwa backup code dipakai (kalau bukan dia, alert!)
await notifyUser(userId, "Backup code used for MFA bypass");
return true;
}
}
return false;
}
UI pattern yang baik:
- Setup MFA → tampilkan 10 kode di 1 halaman dengan tombol "Download PDF"
- User centang "Saya sudah menyimpan kode ini" sebelum lanjut
- Di settings, user bisa regenerate kode baru (invalidate yang lama)
- Counter "X of 10 remaining" di UI — kalau tinggal 2, tawarkan regenerate
Pitfall yang sering terjadi:
❌ Simpan plain codes di DB — kalau DB bocor, attacker langsung bypass MFA semua user. Hash seperti password.
❌ Regenerate tanpa invalidate old — user bikin kode baru tapi kode lama masih valid = security hole.
❌ Tidak notify saat backup code dipakai — attacker yang dapat 1 kode bisa pakai diam-diam. Selalu kirim alert email/SMS.
❌ Short codes (4 digit) — brute-forceable. Minimal 8 hex char (32-bit entropy) + rate limit.
❌ Reuse code untuk recovery password — backup MFA code berbeda dengan password recovery. Jangan campur scope.
Alur login dengan MFA:
1. User masukkan email + password
2. Server verifikasi password → OK
3. Server cek: user punya MFA aktif?
- Tidak → login selesai, beri token
- Ya → kirim response "MFA required"
4. Client tampilkan input kode MFA
5. User masukkan kode dari authenticator app
6. Server verifikasi kode TOTP
7. Login selesai, beri token dengan mfa_verified: true
Best practices:
- Selalu sediakan recovery codes saat setup MFA
- Jangan paksa MFA untuk semua user — tapi tawarkan dan edukasi
- Untuk admin/roles sensitif, MFA wajib
- Rate limit percobaan kode MFA (cegah brute force 000000-999999)
- TOTP lebih aman dari SMS OTP — prioritaskan TOTP/WebAuthn