Normalize admin routes and docker config

This commit is contained in:
Lectom C Han
2026-02-04 12:40:02 +09:00
parent bf86b1d1e7
commit 410b2b7b48
30 changed files with 467 additions and 231 deletions

View File

@@ -1,12 +1,17 @@
<?php
// /kngil/auth/oidc-callback.php
session_start();
ini_set('log_errors', '1');
ini_set('error_log', '/proc/self/fd/2');
require_once dirname(__DIR__) . '/vendor/autoload.php';
require_once dirname(__DIR__) . '/bbs/db_conn.php';
$config = require_once dirname(__DIR__) . '/bbs/oidc_config.php';
use Jumbojett\OpenIDConnectClient;
$usersTable = 'kngil.users';
$membersTable = 'kngil.members';
$oidc = new OpenIDConnectClient(
$config['issuer'],
$config['client_id'],
@@ -16,16 +21,124 @@ $oidc = new OpenIDConnectClient(
$oidc->setRedirectURL($config['redirect_url']);
try {
$stmt = $pdo->query("SELECT to_regclass('kngil.users') AS reg");
$reg = $stmt ? $stmt->fetchColumn() : null;
if (!$reg) {
$stmt = $pdo->query("SELECT to_regclass('public.users') AS reg");
$reg = $stmt ? $stmt->fetchColumn() : null;
if ($reg) {
$usersTable = 'public.users';
$membersTable = 'public.members';
} else {
throw new Exception(
"사용자 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다. "
. "docker compose down -v 후 다시 실행하거나, "
. "DB_NAME/DB_USER/DB_PASS 설정을 확인하세요."
);
}
}
$memberReg = $pdo->query("SELECT to_regclass('{$membersTable}') AS reg");
$memberReg = $memberReg ? $memberReg->fetchColumn() : null;
if (!$memberReg) {
$altMembersTable = $membersTable === 'kngil.members' ? 'public.members' : 'kngil.members';
$memberReg = $pdo->query("SELECT to_regclass('{$altMembersTable}') AS reg");
$memberReg = $memberReg ? $memberReg->fetchColumn() : null;
if ($memberReg) {
$membersTable = $altMembersTable;
} else {
throw new Exception("회원 테이블을 찾을 수 없습니다. DB 초기화가 필요합니다.");
}
}
$pdo->exec("ALTER TABLE {$usersTable} ADD COLUMN IF NOT EXISTS oidc_sub VARCHAR(255) UNIQUE");
if (!$oidc->authenticate()) {
throw new Exception("Authentication failed");
}
$userInfo = $oidc->requestUserInfo();
$idToken = $oidc->getIdToken();
$accessToken = $oidc->getAccessToken();
$jwtClaims = [];
if (!empty($idToken)) {
$parts = explode('.', $idToken);
if (count($parts) >= 2) {
$payload = strtr($parts[1], '-_', '+/');
$padding = 4 - (strlen($payload) % 4);
if ($padding < 4) {
$payload .= str_repeat('=', $padding);
}
$decoded = json_decode(base64_decode($payload), true);
if (is_array($decoded)) {
$jwtClaims = $decoded;
}
}
}
// 디버그용: ID 토큰 확보 여부 로그 출력 (파일)
$logDir = dirname(__DIR__) . '/log';
if (!is_dir($logDir)) {
@mkdir($logDir, 0775, true);
}
$logPath = $logDir . '/oidc_debug.log';
if (!is_writable($logDir)) {
$logPath = '/tmp/oidc_debug.log';
error_log('[OIDC_DEBUG] log_dir_not_writable, fallback=/tmp/oidc_debug.log');
}
$tokenInfo = empty($idToken) ? 'MISSING' : ('PRESENT len=' . strlen($idToken));
$claimKeys = empty($jwtClaims) ? 'none' : implode(',', array_keys($jwtClaims));
$logLine = sprintf(
"[%s] host=%s uri=%s sid=%s id_token=%s claims=%s\n",
date('c'),
$_SERVER['HTTP_HOST'] ?? '-',
$_SERVER['REQUEST_URI'] ?? '-',
session_id(),
$tokenInfo,
$claimKeys
);
$writeOk = @file_put_contents($logPath, $logLine, FILE_APPEND);
if ($writeOk === false) {
error_log('[OIDC_DEBUG] log_write_failed path=' . $logPath);
}
// 디버그용: userInfo/claims 전체 덤프 (토큰 제외)
$dump = [
'userInfo' => $userInfo,
'jwtClaims' => $jwtClaims
];
$dumpLine = sprintf(
"[%s] oidc_dump=%s\n",
date('c'),
json_encode($dump, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
$dumpOk = @file_put_contents($logPath, $dumpLine, FILE_APPEND);
if ($dumpOk === false) {
error_log('[OIDC_DEBUG] dump_write_failed path=' . $logPath);
}
// 도커 로그로도 출력
error_log('[OIDC_DEBUG] ' . $dumpLine);
// $userInfo 에 포함된 데이터 예시: sub, email, name, preferred_username 등
$email = $userInfo->email ?? null;
$sub = $userInfo->sub ?? null; // IDP 고유 식별자
$name = $userInfo->name ?? ($userInfo->preferred_username ?? 'Unknown');
$preferred = $userInfo->preferred_username ?? null;
$name = $userInfo->name ?? null;
if (!$email && isset($jwtClaims['email'])) {
$email = $jwtClaims['email'];
}
if (!$name && isset($jwtClaims['name'])) {
$name = $jwtClaims['name'];
}
if (!$name && $preferred) {
$name = $preferred;
}
if (!$name && $email) {
$name = $email;
}
if (!$name && $sub) {
$seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)$sub));
$name = 'oidc_' . substr($seed, 0, 10);
}
if (!$email && !$sub) {
throw new Exception("IDP provided insufficient user information.");
@@ -33,7 +146,7 @@ try {
// 1. 사용자 매핑 (sub 또는 email 기준)
$stmt = $pdo->prepare("
SELECT * FROM kngil.users
SELECT * FROM {$usersTable}
WHERE (oidc_sub = :sub OR LOWER(email) = LOWER(:email))
AND use_yn = 'Y'
LIMIT 1
@@ -42,14 +155,99 @@ try {
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$user) {
// [정책 선택] 새 사용자 자동 생성 또는 로그인 거부
// 여기서는 예시로 로그인 거부 처리
throw new Exception("등록되지 않은 사용자입니다. 관리자에게 문의하세요. (IDP: $email)");
$defaultMemberId = getenv('OIDC_DEFAULT_MEMBER_ID') ?: '';
if ($defaultMemberId !== '') {
$checkMember = $pdo->prepare("SELECT 1 FROM {$membersTable} WHERE member_id = :member_id LIMIT 1");
$checkMember->execute([':member_id' => $defaultMemberId]);
if (!$checkMember->fetchColumn()) {
throw new Exception("OIDC_DEFAULT_MEMBER_ID가 members에 존재하지 않습니다: {$defaultMemberId}");
}
} else {
$memberStmt = $pdo->query("SELECT member_id FROM {$membersTable} ORDER BY member_id ASC LIMIT 1");
$defaultMemberId = $memberStmt ? $memberStmt->fetchColumn() : '';
if (!$defaultMemberId) {
throw new Exception("기본 member_id를 찾을 수 없습니다. OIDC_DEFAULT_MEMBER_ID를 설정하세요.");
}
}
$defaultAuth = getenv('OIDC_DEFAULT_AUTH_BC') ?: 'BS100500';
$baseId = $userInfo->preferred_username ?? ($email ? explode('@', $email)[0] : '');
$baseId = strtolower(preg_replace('/[^a-z0-9]/', '', $baseId));
if ($baseId === '') {
$seed = strtolower(preg_replace('/[^a-z0-9]/', '', (string)($sub ?? 'oidc')));
$baseId = 'oidc' . substr($seed, 0, 10);
}
$baseId = substr($baseId, 0, 16);
$userId = $baseId;
$existsStmt = $pdo->prepare("SELECT 1 FROM {$usersTable} WHERE LOWER(user_id) = LOWER(:user_id) LIMIT 1");
$suffix = 1;
while (true) {
$existsStmt->execute([':user_id' => $userId]);
if (!$existsStmt->fetchColumn()) {
break;
}
$tail = sprintf('%02d', $suffix);
$userId = substr($baseId, 0, 20 - strlen($tail)) . $tail;
$suffix++;
if ($suffix > 99) {
$userId = 'oidc' . bin2hex(random_bytes(4));
$userId = substr($userId, 0, 20);
}
}
$userNm = $name ?: ($email ?: $userId);
$rawPhone = $userInfo->phone_number ?? '';
$digits = preg_replace('/\D/', '', $rawPhone);
if (strlen($digits) === 11) {
$telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 4) . '-' . substr($digits, 7, 4);
} elseif (strlen($digits) === 10) {
$telNo = substr($digits, 0, 3) . '-' . substr($digits, 3, 3) . '-' . substr($digits, 6, 4);
} else {
$telNo = '000-0000-0000';
}
$insert = $pdo->prepare("
INSERT INTO {$usersTable} (
member_id, user_id, user_pw, user_nm,
dept_nm, posit_nm, tel_no, email,
auth_bc, use_yn, rmks,
cid, cdt, mid, mdt, oidc_sub
) VALUES (
:member_id, :user_id, NULL, :user_nm,
:dept_nm, :posit_nm, :tel_no, :email,
:auth_bc, 'Y', :rmks,
:cid, CURRENT_TIMESTAMP, :mid, CURRENT_TIMESTAMP, :oidc_sub
)
");
$insert->execute([
':member_id' => $defaultMemberId,
':user_id' => $userId,
':user_nm' => $userNm,
':dept_nm' => $userInfo->department ?? null,
':posit_nm' => $userInfo->title ?? null,
':tel_no' => $telNo,
':email' => $email,
':auth_bc' => $defaultAuth,
':rmks' => 'OIDC auto-registered',
':cid' => $userId,
':mid' => $userId,
':oidc_sub' => $sub
]);
$stmt = $pdo->prepare("
SELECT * FROM {$usersTable}
WHERE LOWER(user_id) = LOWER(:user_id)
LIMIT 1
");
$stmt->execute([':user_id' => $userId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
}
// 2. oidc_sub 업데이트 (최초 연동 시)
if (empty($user['oidc_sub']) && $sub) {
$upd = $pdo->prepare("UPDATE kngil.users SET oidc_sub = :sub WHERE user_id = :id");
$upd = $pdo->prepare("UPDATE {$usersTable} SET oidc_sub = :sub WHERE user_id = :id");
$upd->execute([':sub' => $sub, ':id' => $user['user_id']]);
}
@@ -63,20 +261,42 @@ try {
'dept_nm' => $user['dept_nm'] ?? null,
'tel_no' => $user['tel_no'] ?? null,
'email' => $user['email'] ?? null,
'idp_name' => $name ?: null,
'idp_email' => $email ?? null,
'idp_id_token' => $idToken ?? null,
'idp_access_token' => $accessToken ?? null,
'idp_claims' => $jwtClaims ?? null,
'oidc_mode' => true // OIDC 로그인을 나타내는 플래그
];
// 로그인 완료 후 부모 창에 알리고 종료
session_write_close();
// 로그인 완료 후 부모 창에 알리고 종료 (팝업이 아닐 경우 메인으로 이동)
?>
<!DOCTYPE html>
<html>
<body>
<script>
if (window.opener) {
window.opener.postMessage({ type: 'OIDC_LOGIN_SUCCESS' }, window.location.origin);
}
window.close();
(function () {
const target = '/kngil/skin/index.php';
if (window.opener && !window.opener.closed) {
try {
window.opener.postMessage({ type: 'OIDC_LOGIN_SUCCESS' }, window.location.origin);
} catch (e) {
// 팝업 차단/보안 정책으로 실패할 수 있어 무시합니다.
}
window.close();
setTimeout(function () {
window.location.href = target;
}, 300);
} else {
window.location.href = target;
}
})();
</script>
<noscript>
<a href="/kngil/skin/index.php">메인으로 이동</a>
</noscript>
</body>
</html>
<?php