= OTP_MAX_FAILS || login_locked()) { unset($_SESSION['pre_auth']); redirect(base_url('admin/login.php?reason=otp')); } if (preg_match('/^[A-Z2-7]{4}-?[A-Z2-7]{4}$/', $code)) { // One-time recovery code (lost phone) $hashes = json_decode((string)$row['recovery_codes'], true) ?: []; $i = array_search(recovery_hash($code), $hashes, true); if ($i !== false) { array_splice($hashes, (int)$i, 1); db()->prepare('UPDATE admins SET recovery_codes = ? WHERE id = ?')->execute([json_encode($hashes), $row['id']]); record_otp_attempt($row['username'], true); complete_login($row, 'kode pemulihan'); flash('info', 'Anda masuk dengan kode pemulihan. Sisa kode: ' . count($hashes) . '. Minta super admin me-reset OTP bila ponsel hilang.'); redirect(base_url('admin/')); } } elseif ($step = totp_verify(secret_decrypt((string)$row['otp_secret']), $code, $row['otp_last_step'] !== null ? (int)$row['otp_last_step'] : null)) { db()->prepare('UPDATE admins SET otp_last_step = ? WHERE id = ?')->execute([$step, $row['id']]); record_otp_attempt($row['username'], true); complete_login($row, 'OTP'); redirect(base_url('admin/')); } record_otp_attempt($row['username'], false); $left = OTP_MAX_FAILS - otp_failures($row['username']); $error = $left > 0 ? "Kode salah atau sudah dipakai. Sisa percobaan: $left." : 'Terlalu banyak kode salah.'; if ($left <= 0) { unset($_SESSION['pre_auth']); redirect(base_url('admin/login.php?reason=otp')); } } if ($mode === 'enroll') { app_key(); // fail early when the encryption key is not configured $_SESSION['pre_auth']['exp'] = time() + 2 * PRE_AUTH_TTL; // time to install the app if (empty($_SESSION['pre_auth']['secret'])) { $_SESSION['pre_auth']['secret'] = totp_new_secret(); } $secret = $_SESSION['pre_auth']['secret']; if ($_SERVER['REQUEST_METHOD'] === 'POST') { csrf_check(); if (otp_failures($row['username']) >= OTP_MAX_FAILS || login_locked()) { unset($_SESSION['pre_auth']); redirect(base_url('admin/login.php?reason=otp')); } $step = totp_verify($secret, input_str($_POST['code'] ?? ''), null); if ($step) { [$plain, $hashes] = recovery_codes_new(); db()->prepare('UPDATE admins SET otp_secret = ?, otp_enabled = 1, otp_last_step = ?, recovery_codes = ? WHERE id = ?') ->execute([secret_encrypt($secret), $step, json_encode($hashes), $row['id']]); record_otp_attempt($row['username'], true); complete_login($row, 'OTP, aktivasi'); audit('user.otp', 'Mengaktifkan autentikasi dua faktor (OTP)'); $_SESSION['recovery_once'] = $plain; redirect(base_url('admin/2fa.php?done=1')); } record_otp_attempt($row['username'], false); $error = 'Kode tidak cocok. Pastikan jam ponsel sudah otomatis, lalu ketik kode yang sedang tampil.'; } } } catch (RuntimeException $ex) { error_log('[serubi] 2fa: ' . $ex->getMessage()); $configError = 'Autentikasi dua faktor belum dapat dipakai karena konfigurasi server belum lengkap (security.app_key). Hubungi pengelola server.'; } ?> Verifikasi dua langkah ยท SERUBI
SERUBI

Simpan kode pemulihan

Autentikasi dua faktor aktif. Bila ponsel hilang, masuk dengan salah satu kode di bawah (masing-masing hanya bisa dipakai sekali). Kode ini hanya ditampilkan sekarang: cetak atau simpan di tempat aman.

Sudah saya simpan, lanjut ke admin

Aktifkan autentikasi dua faktor

Login admin wajib memakai kode OTP dari aplikasi authenticator (Google Authenticator, Microsoft Authenticator, atau sejenisnya).

Verifikasi dua langkah

Ketik 6 digit kode dari aplikasi authenticator untuk akun @.

  1. Buka aplikasi authenticator, pilih tambah akun, lalu pindai QR code ini.
  2. Tidak bisa memindai? Masukkan kunci ini secara manual (tipe: berbasis waktu):
  3. Ketik 6 digit kode yang muncul di aplikasi.

Ponsel hilang? Ketik salah satu kode pemulihan (format XXXX-XXXX).