diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index ee17ac45b5ed8ce8e475a08a8a59eee55d999dff..03acaf2170c44bb833276b336f300662affd5718 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -7,6 +7,7 @@ use OCA\EcloudAccounts\AppInfo\Application; use OCA\EcloudAccounts\Db\TwoFactorMapper; use OCA\EcloudAccounts\Exception\SSOAdminAccessTokenException; use OCA\EcloudAccounts\Exception\SSOAdminAPIException; +use OCP\ICacheFactory; use OCP\IConfig; use OCP\ILogger; use OCP\IUserManager; @@ -27,6 +28,7 @@ class SSOService { private IFactory $l10nFactory; private IUserManager $userManager; private TwoFactorMapper $twoFactorMapper; + private ICacheFactory $cacheFactory; private string $mainDomain; private string $legacyDomain; @@ -34,8 +36,9 @@ class SSOService { private const ADMIN_TOKEN_ENDPOINT = '/auth/realms/master/protocol/openid-connect/token'; private const USERS_ENDPOINT = '/users'; private const CREDENTIALS_ENDPOINT = '/users/{USER_ID}/credentials'; + private const CLIENTS_CACHE_TTL = 6 * 3600; // 6 hours - public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, TwoFactorMapper $twoFactorMapper, ILogger $logger) { + public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, TwoFactorMapper $twoFactorMapper, ILogger $logger, ICacheFactory $cacheFactory) { $this->appName = $appName; $this->config = $config; @@ -56,6 +59,7 @@ class SSOService { $this->l10nFactory = $l10nFactory; $this->userManager = $userManager; $this->twoFactorMapper = $twoFactorMapper; + $this->cacheFactory = $cacheFactory; $this->mainDomain = $this->config->getSystemValue("main_domain"); $this->legacyDomain = $this->config->getSystemValue("legacy_domain"); @@ -110,6 +114,130 @@ class SSOService { $this->logger->debug('logout calling SSO API with url: '. $url); $this->callSSOAPI($url, 'POST', [], 204); + + // Revoke offline sessions for all clients + try { + $this->revokeOfflineSessionsForUser(); + } catch (\Exception $e) { + $this->logger->warning('Failed to revoke offline sessions: ' . $e->getMessage()); + } + } + + /** + * Revoke all offline sessions for the current user in Keycloak + */ + private function revokeOfflineSessionsForUser(): void { + try { + $clients = $this->getClientsForRealm(); + if (empty($clients)) { + return; + } + + foreach ($clients as $client) { + try { + $this->revokeOfflineSessionsForClient($client); + } catch (\Exception $e) { + $this->logger->warning('Failed to revoke offline sessions for client: ' . (isset($client['id']) ? $client['id'] : 'unknown') . ' - ' . $e->getMessage()); + // Continue with other clients even if one fails + } + } + } catch (\Exception $e) { + $this->logger->warning('Failed to get clients for offline session revocation: ' . $e->getMessage()); + } + } + + /** + * Get all clients for the current realm with caching + */ + private function getClientsForRealm(): array { + // Create unique cache key based on URL (no parameters for this endpoint) + $clientsUrl = $this->ssoConfig['admin_rest_api_url'] . '/clients'; + $cacheKey = "{$clientsUrl}|no_params|no_keys"; + + // Try to get cached clients + $clientsCache = $this->cacheFactory->createDistributed('ecloud_accounts_realm_clients'); + $cachedClients = $clientsCache->get($cacheKey); + if ($cachedClients !== null) { + $this->logger->debug('Returning cached clients from distributed cache'); + return $cachedClients; + } + + try { + $clients = $this->callSSOAPI($clientsUrl, 'GET'); + + if (!is_array($clients)) { + $this->logger->error('Could not fetch clients for offline session revocation.'); + return []; + } + + $clientsCache->set($cacheKey, $clients, self::CLIENTS_CACHE_TTL); + + return $clients; + } catch (\Exception $e) { + $this->logger->error('Failed to fetch clients for offline session revocation: ' . $e->getMessage()); + return []; + } + } + + /** + * Revoke offline sessions for a specific client + */ + private function revokeOfflineSessionsForClient(array $client): void { + if (!isset($client['id'])) { + return; + } + + $clientUUID = $client['id']; + + // Get offline sessions for this client and user + $offlineSessions = $this->getOfflineSessionsForClient($clientUUID); + if (empty($offlineSessions)) { + $this->logger->debug('No offline sessions found for client: ' . $clientUUID); + return; + } + + // Delete each offline session individually + foreach ($offlineSessions as $session) { + $this->deleteOfflineSession($session); + } + } + + /** + * Get offline sessions for a specific client and user + */ + private function getOfflineSessionsForClient(string $clientUUID): array { + $offlineSessionsUrl = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId . '/offline-sessions/' . $clientUUID . '?isOffline=true'; + + try { + $offlineSessions = $this->callSSOAPI($offlineSessionsUrl, 'GET'); + + if (!is_array($offlineSessions)) { + return []; + } + + return $offlineSessions; + } catch (\Exception $e) { + $this->logger->error('Failed to fetch offline sessions for client ' . $clientUUID . ': ' . $e->getMessage()); + return []; + } + } + + /** + * Delete a specific offline session + */ + private function deleteOfflineSession(array $session): void { + if (!isset($session['id'])) { + return; + } + + $sessionId = $session['id']; + $deleteSessionUrl = $this->ssoConfig['admin_rest_api_url'] . '/sessions/' . $sessionId . '?isOffline=true'; + + try { + $this->callSSOAPI($deleteSessionUrl, 'DELETE', [], 204); + } catch (\Exception $e) { + $this->logger->error('Failed to delete offline session ' . $sessionId . ': ' . $e->getMessage()); + } } public function handle2FAStateChange(bool $enabled, string $username) : void {