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(); // 로그인 완료 후 부모 창에 알리고 종료 (팝업이 아닐 경우 메인으로 이동) ?>
로그인 오류"; echo "" . htmlspecialchars($e->getMessage()) . "
"; echo "메인으로 돌아가기"; }