Multi-factor Authentication (MFA) — Auth

Multi-factor Authentication (MFA) menambahkan lapisan keamanan ekstra dengan meminta lebih dari satu bentuk verifikasi identitas. Tiga faktor autentikasi: Fakto

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:

  1. Setup MFA → tampilkan 10 kode di 1 halaman dengan tombol "Download PDF"
  2. User centang "Saya sudah menyimpan kode ini" sebelum lanjut
  3. Di settings, user bisa regenerate kode baru (invalidate yang lama)
  4. 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: