Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8b34ecc7 authored by Ronak Patel's avatar Ronak Patel
Browse files

Merge branch 'dev/offline-sso-issue-clean' into 'main'

Revoke offline sessions when logout

See merge request !208
parents d99f3f20 06d36823
Loading
Loading
Loading
Loading
Loading
+129 −1
Original line number Diff line number Diff line
@@ -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 {