310 lines
11 KiB
PHP
310 lines
11 KiB
PHP
<?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'],
|
|
$config['client_secret']
|
|
);
|
|
|
|
$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 고유 식별자
|
|
$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.");
|
|
}
|
|
|
|
// 1. 사용자 매핑 (sub 또는 email 기준)
|
|
$stmt = $pdo->prepare("
|
|
SELECT * FROM {$usersTable}
|
|
WHERE (oidc_sub = :sub OR LOWER(email) = LOWER(:email))
|
|
AND use_yn = 'Y'
|
|
LIMIT 1
|
|
");
|
|
$stmt->execute([':sub' => $sub, ':email' => $email]);
|
|
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$user) {
|
|
$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 {$usersTable} SET oidc_sub = :sub WHERE user_id = :id");
|
|
$upd->execute([':sub' => $sub, ':id' => $user['user_id']]);
|
|
}
|
|
|
|
// 3. 세션 설정 (bbs/login.php 와 동일한 구조)
|
|
$_SESSION['login'] = [
|
|
'member_id' => $user['member_id'],
|
|
'user_id' => $user['user_id'],
|
|
'user_nm' => $user['user_nm'],
|
|
'auth_bc' => $user['auth_bc'],
|
|
'co_nm' => $user['co_nm'] ?? null,
|
|
'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>
|
|
(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
|
|
exit;
|
|
|
|
} catch (Exception $e) {
|
|
echo "<h1>로그인 오류</h1>";
|
|
echo "<p>" . htmlspecialchars($e->getMessage()) . "</p>";
|
|
echo "<a href='/kngil/index.php'>메인으로 돌아가기</a>";
|
|
}
|