rtrim($cfg['base_url'], '/'), 'realm' => $cfg['realm'], 'clientId' => $cfg['client_id'], 'clientSecret' => $cfg['client_secret'], 'redirectUri' => $cfg['redirect_uri'], ]); } /** * Decode user claims from the access token JWT payload. * Avoids calling the userinfo endpoint, which Keycloak may return as a * signed JWT (application/jwt) rather than JSON — causing decryption errors. * * @return array */ public static function claimsFromToken(string $jwt): array { $parts = explode('.', $jwt); if (count($parts) < 2) { return []; } $payload = base64_decode(strtr($parts[1], '-_', '+/'), true); if ($payload === false) { return []; } $data = json_decode($payload, true); return is_array($data) ? $data : []; } public static function requireLogin(): ?Response { if (!self::isLoggedIn()) { $_SESSION['auth_return_to'] = $_SERVER['REQUEST_URI'] ?? '/'; return Response::redirect('/auth/login'); } return null; } public static function isLoggedIn(): bool { return !empty($_SESSION['auth_user']); } public static function getCurrentUser(): array { return $_SESSION['auth_user'] ?? []; } public static function getCurrentUsername(): string { $user = self::getCurrentUser(); return $user['preferred_username'] ?? $user['email'] ?? ''; } public static function storeUser(array $userInfo): void { $_SESSION['auth_user'] = $userInfo; } public static function clearSession(): void { $_SESSION = []; if (ini_get('session.use_cookies')) { $params = session_get_cookie_params(); setcookie( session_name(), '', time() - 42000, $params['path'], $params['domain'], $params['secure'], $params['httponly'] ); } session_destroy(); } public static function logoutUrl(): string { $cfg = self::config(); $base = rtrim($cfg['base_url'], '/'); $realm = $cfg['realm']; $postLogout = urlencode($cfg['post_logout_redirect_uri']); return "{$base}/realms/{$realm}/protocol/openid-connect/logout?redirect_uri={$postLogout}"; } }