app->config()->authEnabled; } public function start(): void { $this->app->session()->start(); } public function user(): ?array { $this->start(); $user = $_SESSION['auth_user'] ?? null; if (!is_array($user)) { return null; } $expiresAt = (int) ($_SESSION['auth_expires_at'] ?? 0); if ($expiresAt > 0 && $expiresAt < time()) { $this->clearLocalSession(); return null; } $_SESSION['auth_expires_at'] = time() + self::SESSION_TTL; return $user; } public function isAuthenticated(): bool { return $this->user() !== null; } public function login(string $returnTo = '/'): void { if (!$this->isEnabled()) { redirect($returnTo !== '' ? $returnTo : '/'); } $this->start(); $_SESSION['oidc_return_to'] = $this->safeReturnTo($returnTo); redirect('/auth/login'); } public function callback(): void { $query = (string) ($_SERVER['QUERY_STRING'] ?? ''); redirect('/auth/callback' . ($query !== '' ? '?' . $query : '')); } public function logout(): void { redirect('/auth/logout'); } public function storeUser(array $claims, array $groups, string $idToken): void { $username = (string) ($claims['preferred_username'] ?? $claims['email'] ?? $claims['sub'] ?? ''); $_SESSION['auth_user'] = [ 'sub' => (string) ($claims['sub'] ?? ''), 'username' => $username, 'email' => (string) ($claims['email'] ?? ''), 'name' => (string) ($claims['name'] ?? $username), 'groups' => $groups, 'id_token' => $idToken, ]; $_SESSION['auth_id_token'] = $idToken; $_SESSION['auth_expires_at'] = time() + self::SESSION_TTL; $this->rememberKeycloakUser($claims, $groups); } public function canAccessModule(array $module): bool { $auth = is_array($module['auth'] ?? null) ? $module['auth'] : []; $required = (bool) ($auth['required'] ?? false); if (!$required || !$this->isEnabled()) { return true; } $user = $this->user(); if ($user === null) { return false; } $allowedUsers = $this->normalizeList($auth['users'] ?? []); $allowedGroups = $this->normalizeList($auth['groups'] ?? []); if ($allowedUsers === [] && $allowedGroups === []) { return true; } $username = strtolower((string) ($user['username'] ?? '')); $email = strtolower((string) ($user['email'] ?? '')); $sub = strtolower((string) ($user['sub'] ?? '')); foreach ($allowedUsers as $allowedUser) { if ($allowedUser === $username || ($email !== '' && $allowedUser === $email) || ($sub !== '' && $allowedUser === $sub)) { return true; } } $userGroups = $this->normalizeList($user['groups'] ?? []); return array_intersect($allowedGroups, $userGroups) !== []; } private function rememberKeycloakUser(array $claims, array $groups): void { $pdo = $this->app->basePdo(); $sub = trim((string)($claims['sub'] ?? '')); if (!$pdo || $sub === '') { return; } $groupsJson = json_encode(array_values(array_unique(array_map('strval', $groups))), JSON_UNESCAPED_UNICODE); if (!is_string($groupsJson)) { $groupsJson = '[]'; } $stmt = $pdo->prepare( "INSERT INTO nexus_auth_users (sub, username, email, name, groups, last_login_at) VALUES (:sub, :username, :email, :name, :groups, CURRENT_TIMESTAMP) ON CONFLICT(sub) DO UPDATE SET username = excluded.username, email = excluded.email, name = excluded.name, groups = excluded.groups, last_login_at = CURRENT_TIMESTAMP" ); $stmt->execute([ 'sub' => $sub, 'username' => (string)($claims['preferred_username'] ?? ''), 'email' => (string)($claims['email'] ?? ''), 'name' => (string)($claims['name'] ?? ''), 'groups' => $groupsJson, ]); } public function requireModuleAccess(array $module): void { if ($this->canAccessModule($module)) { return; } if (!$this->isAuthenticated()) { $this->login($this->currentPath()); } http_response_code(403); exit('Forbidden'); } public function filterModules(array $modules): array { return array_values(array_filter($modules, fn (array $module): bool => $this->canAccessModule($module))); } private function normalizeList(mixed $values): array { if (is_string($values)) { $values = preg_split('/[,\\n]+/', $values) ?: []; } if (!is_array($values)) { $values = [$values]; } $normalized = []; foreach ($values as $value) { $item = strtolower(trim((string) $value)); if ($item !== '') { $normalized[] = $item; } } return array_values(array_unique($normalized)); } private function clearLocalSession(): void { unset($_SESSION['auth_user'], $_SESSION['auth_id_token'], $_SESSION['auth_expires_at']); } private function currentPath(): string { $uri = (string) ($_SERVER['REQUEST_URI'] ?? '/'); return $this->safeReturnTo($uri); } private function safeReturnTo(string $returnTo): string { $returnTo = trim($returnTo); if ($returnTo === '' || !str_starts_with($returnTo, '/') || str_starts_with($returnTo, '//')) { return '/'; } return $returnTo; } }