Multi-factor Authentication (MFA) — Auth

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

Multi-factor authentication (MFA) meminta lebih dari satu bukti saat login, dan bukti-buktinya harus dari jenis yang berbeda. Bayangin ATM: kamu butuh kartu (sesuatu yang kamu punya) dan PIN (sesuatu yang kamu tahu). Kartu yang dicuri gak berguna tanpa PIN, dan PIN yang diintip gak berguna tanpa kartunya. MFA membawa ide itu ke login: password saja gak cukup.

🧪 Tebakan cepat: jam 07.00.10, aplikasi authenticator di HP Bu Dewi menampilkan kode 911338. Dia baru mengirim kode itu jam 07.01.40, 90 detik kemudian. Server Portal Sekolah menerimanya? Tebak dulu, jawabannya ada di bagian "TOTP: kode yang berganti tiap 30 detik".

Tiga jenis faktor

Faktor Artinya Contoh
Sesuatu yang kamu tahu (knowledge) rahasia di kepala password, PIN
Sesuatu yang kamu punya (possession) benda yang kamu pegang HP dengan aplikasi authenticator, security key
Sesuatu yang kamu adalah (inherence) bagian tubuhmu sidik jari, wajah

MFA menggabungkan minimal dua jenis yang berbeda. Password ditambah PIN itu dua hal yang sama-sama "tahu", jadi bukan MFA. Password ditambah kode dari HP itu "tahu" plus "punya": ini 2FA (two-factor authentication), bentuk MFA yang paling umum. Di Portal Sekolah, MFA wajib untuk role admin: akun Bu Dewi bisa menghapus user, jadi password-nya saja gak boleh cukup.

Login dua langkah

Password yang benar belum memberi access token. Server cuma memberi mfa_token: tanda berumur pendek bahwa langkah pertama lolos, yang cuma bisa dipakai di endpoint langkah kedua.

POST /auth/login
{"email": "[email protected]", "password": "rahasia-bu-dewi"}

HTTP/1.1 200 OK
{"mfa_required": true, "mfa_token": "mt_7Qx2", "expires_in": 300}

POST /auth/mfa
{"mfa_token": "mt_7Qx2", "code": "911338", "remember_device": false}

HTTP/1.1 200 OK
Set-Cookie: refresh_token=R1; Max-Age=2592000; Path=/auth/refresh; Secure; HttpOnly; SameSite=Strict
{"access_token": "A1", "token_type": "Bearer", "expires_in": 900}

TOTP: kode yang berganti tiap 30 detik

Faktor kedua yang paling umum adalah TOTP (Time-based One-Time Password, RFC 6238). Saat Bu Dewi mengaktifkan MFA, server membuat secret acak dan menampilkannya sebagai QR code. Isi QR-nya:

otpauth://totp/Portal%20Sekolah:[email protected]?secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ&issuer=Portal%20Sekolah

Aplikasi authenticator (Google Authenticator, Authy, dan lain-lain) menyimpan secret itu. Sejak saat itu HP dan server menghitung kode yang sama, masing-masing sendiri, tanpa saling mengirim apa pun: counter = waktu Unix dibagi 30 lalu dibulatkan ke bawah, lalu HMAC-SHA1 dari counter itu dengan secret, lalu dipotong jadi 6 digit.

async function totp(secretBytes, counter) {
  const msg = new Uint8Array(8);
  new DataView(msg.buffer).setBigUint64(0, BigInt(counter)); // counter, 8 byte big-endian
  const key = await crypto.subtle.importKey("raw", secretBytes, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
  const mac = new Uint8Array(await crypto.subtle.sign("HMAC", key, msg)); // 20 byte
  const offset = mac[19] & 0x0f; // dynamic truncation
  const bin = ((mac[offset] & 0x7f) << 24) | (mac[offset + 1] << 16) | (mac[offset + 2] << 8) | mac[offset + 3];
  return String(bin % 1000000).padStart(6, "0");
}

async function verifyCode(secretBytes, code, unixSeconds) {
  const counter = Math.floor(unixSeconds / 30);
  for (const c of [counter - 1, counter, counter + 1]) { // window ±1
    if ((await totp(secretBytes, c)) === code) return true;
  }
  return false;
}

Server gak cuma mencocokkan counter sekarang. Jam HP dan server bisa selisih sedikit, jadi dia juga menerima satu counter sebelum dan sesudahnya (window ±1). Jam 07.00.10, waktu Unix-nya 1791504010 dan counter-nya 59716800:

Counter Berlaku Kode Diterima jam 07.00.10?
59716799 06.59.30 sampai 06.59.59 839888 ya, window -1
59716800 07.00.00 sampai 07.00.29 911338 ya, counter sekarang
59716801 07.00.30 sampai 07.00.59 736890 ya, window +1

Jam 07.01.40, counter sudah 59716803, jadi yang diterima 736890, 718685, dan 914220. Kode 911338 tiga counter di belakang: ditolak. Itu jawaban tebakannya.

Kenapa TOTP, bukan SMS

Kode SMS dikirim lewat jaringan operator, dan nomor telepon bisa dipindahkan ke kartu SIM lain (SIM swapping): siapa pun yang memegang nomornya menerima kodenya. TOTP dihitung di HP itu sendiri, tanpa jaringan, dari secret yang cuma dibagikan sekali saat setup. Jadi TOTP lebih kuat dari SMS. Yang lebih kuat lagi adalah passkey dan security key, dibahas di lesson Passwordless & Passkeys.

Recovery codes

HP hilang berarti faktor "punya" ikut hilang. Supaya Bu Dewi gak terkunci selamanya, saat MFA diaktifkan server membuat recovery codes: 10 kode sekali pakai, misalnya 7f3a-91c2.

$codes = collect(range(1, 10))->map(fn () => implode('-', str_split(bin2hex(random_bytes(4)), 4)));
$user->recoveryCodes()->delete(); // regenerate: kode lama langsung gak berlaku
foreach ($codes as $code) {
    $user->recoveryCodes()->create(['code_hash' => Hash::make($code)]);
}

return response()->json(['recovery_codes' => $codes]); // satu-satunya kali kode ini terlihat

Kalau ternyata sebuah kode dipakai tanpa sepengetahuan pemiliknya, langkahnya: cabut semua sesi aktif, buat ulang semua recovery codes, dan pastikan pemberitahuan saat kode dipakai sudah ada.

Ingat perangkat ini

Meminta kode di setiap login itu melelahkan. Kalau Bu Dewi mencentang "Ingat perangkat ini" saat langkah kedua, server memasang cookie perangkat:

Set-Cookie: mfa_device=md_1; Max-Age=2592000; Path=/auth/login; Secure; HttpOnly; SameSite=Strict

Login berikutnya dari browser yang sama: password tetap diminta, tapi langkah kedua dilewati selama cookie itu sah (30 hari) dan tercatat di server. Server menyimpan hash-nya, jadi ganti password atau "keluar dari semua perangkat" bisa menghapusnya sekaligus.

⚠️ Jebakan yang sering ketemu

TL;DR: MFA = minimal dua jenis faktor berbeda: tahu (password), punya (HP), adalah (sidik jari). Login jadi dua langkah: password memberi mfa_token 5 menit, kode memberi access token. TOTP: counter = waktu Unix / 30, HMAC-SHA1 dengan secret dari QR, 6 digit, diterima dengan window ±1 (kode 90 detik lalu ditolak). TOTP lebih kuat dari SMS (SIM swapping). Recovery codes: 10 kode, hash bcrypt, tampilkan sekali, sekali pakai, beri tahu saat dipakai. Ingat perangkat: cookie HttpOnly 30 hari yang cuma melewati faktor kedua.

Yang akan kamu pelajari