asd
This commit is contained in:
commit
3e4a056b14
67 files changed
+13926
No files matched your search
+172
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
// Second login step (mandatory 2FA): verify the authenticator code, or set the
|
||||
// authenticator up on the first login. Recovery codes are shown once afterwards.
|
||||
require __DIR__ . '/../includes/auth.php';
|
||||
send_security_headers(true);
|
||||
|
||||
// Recovery codes right after activation (already logged in, shown once).
|
||||
if (current_admin() && isset($_GET['done'])) {
|
||||
$codes = $_SESSION['recovery_once'] ?? null;
|
||||
unset($_SESSION['recovery_once']);
|
||||
if (!$codes) {
|
||||
redirect(base_url('admin/'));
|
||||
}
|
||||
$mode = 'done';
|
||||
} elseif (current_admin()) {
|
||||
redirect(base_url('admin/'));
|
||||
} else {
|
||||
$pending = pending_login();
|
||||
if (!$pending) {
|
||||
redirect(base_url('admin/login.php?reason=expired'));
|
||||
}
|
||||
[$row, $mode] = $pending;
|
||||
}
|
||||
|
||||
$error = null;
|
||||
$configError = null;
|
||||
|
||||
try {
|
||||
if ($mode === 'verify' && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
csrf_check();
|
||||
$code = strtoupper(trim(input_str($_POST['code'] ?? '')));
|
||||
if (otp_failures($row['username']) >= 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.';
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Verifikasi dua langkah · SERUBI</title>
|
||||
<?= theme_boot_script() ?>
|
||||
<link rel="icon" href="<?= e(asset('assets/favicon.svg')) ?>" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="<?= e(asset('assets/css/base.css')) ?>">
|
||||
<link rel="stylesheet" href="<?= e(asset('assets/css/admin.css')) ?>">
|
||||
</head>
|
||||
<body class="page-login">
|
||||
<main class="login">
|
||||
<a class="brand" href="<?= e(base_url()) ?>" aria-label="Bank Papua SERUBI, kembali ke kalkulator"><?= bank_logo('auto', 'logo-lg') ?><span class="brand-prog">SERUBI</span></a>
|
||||
|
||||
<?php if ($mode === 'done'): ?>
|
||||
<section class="login-card">
|
||||
<h1>Simpan kode pemulihan</h1>
|
||||
<p class="muted">Autentikasi dua faktor aktif. Bila ponsel hilang, masuk dengan salah satu kode di bawah (masing-masing hanya bisa dipakai sekali). Kode ini <strong>hanya ditampilkan sekarang</strong>: cetak atau simpan di tempat aman.</p>
|
||||
<ol class="recovery-codes">
|
||||
<?php foreach ($codes as $c): ?><li><code><?= e($c) ?></code></li><?php endforeach; ?>
|
||||
</ol>
|
||||
<a class="btn btn-primary btn-block" href="<?= e(base_url('admin/')) ?>"><?= icon('check') ?><span>Sudah saya simpan, lanjut ke admin</span></a>
|
||||
</section>
|
||||
|
||||
<?php else: ?>
|
||||
<form class="login-card" method="post" novalidate>
|
||||
<?= csrf_field() ?>
|
||||
<?php if ($mode === 'enroll'): ?>
|
||||
<h1>Aktifkan autentikasi dua faktor</h1>
|
||||
<p class="muted">Login admin wajib memakai kode OTP dari aplikasi authenticator (Google Authenticator, Microsoft Authenticator, atau sejenisnya).</p>
|
||||
<?php else: ?>
|
||||
<h1>Verifikasi dua langkah</h1>
|
||||
<p class="muted">Ketik 6 digit kode dari aplikasi authenticator untuk akun <strong>@<?= e($row['username']) ?></strong>.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($configError): ?>
|
||||
<p class="alert alert-error" role="alert"><?= icon('alert') ?><span><?= e($configError) ?></span></p>
|
||||
<?php else: ?>
|
||||
<?php if ($error): ?>
|
||||
<p class="alert alert-error" role="alert"><?= icon('alert') ?><span><?= e($error) ?></span></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mode === 'enroll'): ?>
|
||||
<ol class="otp-steps">
|
||||
<li>Buka aplikasi authenticator, pilih tambah akun, lalu pindai QR code ini.
|
||||
<div class="otp-qr" data-otpauth="<?= e(totp_uri($secret, $row['username'])) ?>" role="img" aria-label="QR code untuk aplikasi authenticator"></div>
|
||||
</li>
|
||||
<li>Tidak bisa memindai? Masukkan kunci ini secara manual (tipe: berbasis waktu):
|
||||
<code class="otp-secret"><?= e(trim(chunk_split($secret, 4, ' '))) ?></code>
|
||||
</li>
|
||||
<li>Ketik 6 digit kode yang muncul di aplikasi.</li>
|
||||
</ol>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="code"><?= $mode === 'enroll' ? 'Kode dari aplikasi' : 'Kode OTP' ?></label>
|
||||
<input id="code" name="code" required autofocus autocomplete="one-time-code" inputmode="<?= $mode === 'enroll' ? 'numeric' : 'text' ?>" maxlength="9" spellcheck="false" autocapitalize="characters">
|
||||
<?php if ($mode === 'verify'): ?><p class="hint">Ponsel hilang? Ketik salah satu kode pemulihan (format XXXX-XXXX).</p><?php endif; ?>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-block" type="submit"><?= icon('lock') ?><span><?= $mode === 'enroll' ? 'Aktifkan & masuk' : 'Verifikasi' ?></span></button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
<form method="post" action="<?= e(base_url('admin/logout.php')) ?>">
|
||||
<?= csrf_field() ?>
|
||||
<button type="submit" class="back-link text-btn"><?= icon('x') ?><span>Batal</span></button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
<script src="<?= e(asset('assets/js/lib/qrcode.js')) ?>"></script>
|
||||
<script src="<?= e(asset('assets/js/otp.js')) ?>"></script>
|
||||
<script src="<?= e(asset('assets/js/theme.js')) ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user