Loading lib/Service/SSOService.php +129 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -27,6 +28,7 @@ class SSOService { private IFactory $l10nFactory; private IUserManager $userManager; private TwoFactorMapper $twoFactorMapper; private ICacheFactory $cacheFactory; private string $mainDomain; private string $legacyDomain; Loading @@ -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; Loading @@ -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"); Loading Loading @@ -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 { Loading Loading
lib/Service/SSOService.php +129 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -27,6 +28,7 @@ class SSOService { private IFactory $l10nFactory; private IUserManager $userManager; private TwoFactorMapper $twoFactorMapper; private ICacheFactory $cacheFactory; private string $mainDomain; private string $legacyDomain; Loading @@ -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; Loading @@ -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"); Loading Loading @@ -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 { Loading