관리자 사이트 7페이지의 파편화된 인라인 스타일(색상 리터럴 160개, 페이지별 <style>)을 단일 디자인시스템으로 통합. 신규 php/assets/app.css — "Slate NOC Console": - CSS 변수 단일 토큰(라이트/다크 prefers-color-scheme/인쇄 강제라이트). - 한글 시스템폰트 우선(폐쇄망 웹폰트 미로드 → 오프라인 일관 렌더), 외부 리소스 0. - 상태 3중 인코딩(색+도형[점/좌측레일/글리프]+텍스트) — 색맹·흑백 인쇄 대비. - WCAG AA 대비(토큰 쌍 실측), 본문/캡션 하한 .8125rem. 7페이지 리팩터(dashboard/login/monthly_report/security_evidence/setup_wizard/ setup_mfa/blocked): 인라인 <style> 제거→app.css 링크, PHP 색 삼항→클래스, 생 hex 0, PHP 로직 무변경. make_deploy_bundles.py: TEXT_EXTS += .css. 적대적 다차원 리뷰(미학·CSS아키텍처·통합일관성·a11y·slop) 확정 18건 반영: - [HIGH] .ok 배너 셀렉터가 .badge.ok/.quick-tile.ok 와 충돌 → 정상 배지가 사각형으로 깨지던 것 .banner-ok 네임스페이스로 수정. - 자동새로고침 일시정지 토글(WCAG 2.2.1)+스크롤/details 복원, 차트 임계선+ 최신값 마커, 다크 헤더 슬레이트화, 그림자 절제, 정상 레일 중립화(예외만 컬러), 지표칩 중립화+저습 .dry 분리, 진단 severity 텍스트 라벨, forced-colors 포커스, <main> 랜드마크, role=alert/status, ink-3 대비 상향, 인쇄 칩색 제거 등. 검증: 7페이지 php -l clean, 생 hex 0, 외부참조 0. 라이트/다크 헤드리스 렌더 확인.
127 lines
5.7 KiB
PHP
127 lines
5.7 KiB
PHP
<?php
|
|
// =============================================================================
|
|
// setup_mfa.php - Google Authenticator compatible TOTP enrollment helper
|
|
// Presents an otpauth:// enrollment URI without sending the secret to Google.
|
|
// =============================================================================
|
|
|
|
require_once __DIR__ . '/config.php';
|
|
require_once __DIR__ . '/admin_security.php';
|
|
|
|
ini_set('session.cookie_httponly', 1);
|
|
ini_set('session.cookie_secure', 1);
|
|
ini_set('session.cookie_samesite', 'Strict');
|
|
ini_set('session.use_strict_mode', 1);
|
|
session_start();
|
|
|
|
function setup_mfa_allowed(): bool {
|
|
if (!empty($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
|
|
return true;
|
|
}
|
|
// 토큰 경로는 초기 등록 전에만 허용한다. MFA가 이미 설정된 상태에서
|
|
// MFA_SETUP_TOKEN이 남아 있어도 해당 토큰으로 재등록 화면에 접근할 수 없게 한다.
|
|
// (운영자가 등록 후 토큰을 삭제하지 않았을 때의 잔류 접근 차단.)
|
|
if (admin_mfa_configured()) {
|
|
return false;
|
|
}
|
|
$token = $_GET['token'] ?? $_POST['token'] ?? '';
|
|
return MFA_SETUP_TOKEN !== '' && hash_equals(MFA_SETUP_TOKEN, (string)$token);
|
|
}
|
|
|
|
if (!setup_mfa_allowed()) {
|
|
http_response_code(403);
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo "Forbidden\n";
|
|
exit;
|
|
}
|
|
|
|
if (empty($_SESSION['csrf_token'])) {
|
|
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
|
|
}
|
|
|
|
$issuer = $_POST['issuer'] ?? $_GET['issuer'] ?? 'Leak SMS';
|
|
$account = $_POST['account'] ?? $_GET['account'] ?? ADMIN_USER;
|
|
$secret = $_POST['secret'] ?? $_GET['secret'] ?? admin_totp_secret();
|
|
$secret = strtoupper(preg_replace('/[^A-Z2-7]/i', '', $secret));
|
|
$uri = admin_totp_uri($issuer, $account, $secret);
|
|
$verified = false;
|
|
$error = '';
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) {
|
|
$error = '잘못된 요청입니다. 페이지를 새로고침하세요.';
|
|
} else {
|
|
$verified = admin_totp_verify($secret, $_POST['otp_code'] ?? '', time(), 1);
|
|
if (!$verified) {
|
|
$error = '인증 앱 코드가 일치하지 않습니다.';
|
|
} else {
|
|
admin_audit_log('mfa_setup_verified', $_SESSION['username'] ?? ADMIN_USER, true, [
|
|
'issuer' => $issuer,
|
|
'account' => $account,
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$token = $_GET['token'] ?? $_POST['token'] ?? '';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>관리자 MFA 등록</title>
|
|
<link rel="stylesheet" href="assets/app.css">
|
|
<style>
|
|
/* 페이지 전용: 문서형 좁은 단 (app.css .container는 1320px로 넓음). 값은 토큰만. */
|
|
.mfa-doc { max-width: 760px; margin: var(--sp-6) auto; padding: 0 var(--sp-4); }
|
|
.mfa-doc .panel { margin-bottom: var(--sp-4); }
|
|
.mfa-doc textarea { min-height: 82px; resize: vertical; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="mfa-doc">
|
|
<section class="panel">
|
|
<h1>Google Authenticator MFA 등록</h1>
|
|
<p class="caption">이 화면은 Google Authenticator, Microsoft Authenticator, Authy 등 TOTP 인증 앱에 관리자 계정을 등록하기 위한 도구입니다. 외부 Google API로 비밀키를 보내지 않습니다.</p>
|
|
|
|
<?php if ($error): ?>
|
|
<div class="error" role="alert"><?= htmlspecialchars($error) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($verified): ?>
|
|
<div class="banner-ok" role="status">인증 앱 코드가 확인되었습니다. 아래 설정 줄을 `config.local.php`에 반영하세요.</div>
|
|
<?php endif; ?>
|
|
|
|
<h2>1. 인증 앱에 수동 등록</h2>
|
|
<ol>
|
|
<li>Google Authenticator 앱에서 <strong>설정 키 입력</strong>을 선택합니다.</li>
|
|
<li>계정 이름에 <code><?= htmlspecialchars($issuer . ':' . $account) ?></code>를 입력합니다.</li>
|
|
<li>키에 아래 Base32 비밀키를 입력하고, 시간 기반을 선택합니다.</li>
|
|
</ol>
|
|
<div class="secret"><?= htmlspecialchars($secret) ?></div>
|
|
|
|
<h2>2. 앱 링크 등록</h2>
|
|
<p class="caption">모바일 브라우저에서 열면 인증 앱이 이 URI를 인식할 수 있습니다. 데스크톱에서는 수동 등록을 사용하세요.</p>
|
|
<textarea readonly><?= htmlspecialchars($uri) ?></textarea>
|
|
<p><a class="btn" href="<?= htmlspecialchars($uri) ?>">인증 앱으로 열기</a></p>
|
|
|
|
<h2>3. 6자리 코드 확인</h2>
|
|
<form method="POST">
|
|
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">
|
|
<input type="hidden" name="secret" value="<?= htmlspecialchars($secret) ?>">
|
|
<input type="hidden" name="issuer" value="<?= htmlspecialchars($issuer) ?>">
|
|
<input type="hidden" name="account" value="<?= htmlspecialchars($account) ?>">
|
|
<input type="hidden" name="token" value="<?= htmlspecialchars((string)$token) ?>">
|
|
<label for="otp_code">인증 앱 6자리 코드</label>
|
|
<input type="text" id="otp_code" name="otp_code" required inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code">
|
|
<p><button class="btn" type="submit">코드 확인</button></p>
|
|
</form>
|
|
|
|
<?php if ($verified): ?>
|
|
<h2>4. config.local.php 반영</h2>
|
|
<textarea readonly>'ADMIN_TOTP_SECRET' => '<?= htmlspecialchars($secret) ?>',</textarea>
|
|
<p class="caption">반영 후 `MFA_SETUP_TOKEN`은 빈 값으로 바꾸거나 삭제하세요.</p>
|
|
<?php endif; ?>
|
|
</section>
|
|
</main>
|
|
</body>
|
|
</html>
|