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

Commit 8d782faa authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

feat: for SSO, use already saved KC uid

parent c69d8def
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -10,7 +10,7 @@
    <description><![CDATA[in /e/OS cloud, nextcloud accounts are linked to mail accounts. This app ensures both are coordinated: it sets the e-mail address, quota and storage of the user upon creation.
    It also completes the account deletion by cleaning other parts of the /e/OS cloud setup to ensure no more data is retained when a user requests an account deletion.
    This app uses the UserDeletedEvent to invoke scripts in the docker-welcome container of /e/OS cloud setup]]></description>
    <version>10.1.0</version>
    <version>10.1.1</version>
    <licence>agpl</licence>
    <author mail="dev@murena.com" homepage="https://murena.com/">Murena SAS</author>
    <namespace>EcloudAccounts</namespace>
+61 −0
Original line number Diff line number Diff line
<?php

declare(strict_types=1);

namespace OCA\EcloudAccounts\Cron;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Service\SSOService;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\QueuedJob;
use OCP\ILogger;

// This class retry 2FA state sync with SSO provider.
// It is called from TwoFactorStateChangedListerner when it faced exception on its try.
// This class should retry max 3 times + 1 time tried on TwoFactorStateChangedListerner class.
// In this class, each retry should have interval of 60 seconds.
class TwoFactorStateChangeJob extends QueuedJob {

	public const ENABLED_KEY = 'enabled';
	public const USERNAME_KEY = 'username';
	public const TRYCOUNT_KEY = 'tryCount';
	private const INTERVAL_INBETWEEN_JOB_IN_SEC = 60;

	private SSOService $ssoService;
	private IJobList $jobList;
	private ILogger $logger;

	public function __construct(ITimeFactory $time, IJobList $jobList, SSOService $ssoService, ILogger $logger) {
		parent::__construct($time);

		$this->jobList = $jobList;
		$this->ssoService = $ssoService;
		$this->logger = $logger;

		$this->setAllowParallelRuns(true);
	}

	protected function run($arguments) {
		$enabled = $arguments[self::ENABLED_KEY];
		$username = $arguments[self::USERNAME_KEY];
		$tryCount = $arguments[self::TRYCOUNT_KEY];

		try {
			$this->ssoService->handle2FAStateChange($enabled, $username);
		} catch (Exception $e) {
			if ($tryCount > 2) {
				$this->logger->logException($e, ['app' => Application::APP_ID]);
				return;
			}

			$tryCount = $tryCount + 1;
			$arguments[self::TRYCOUNT_KEY] = $tryCount;
			$this->jobList->scheduleAfter(
				TwoFactorStateChangeJob::class,
				$arguments,
				self::INTERVAL_INBETWEEN_JOB_IN_SEC
			);
		}
	}
}
+15 −14
Original line number Diff line number Diff line
@@ -5,27 +5,27 @@ declare(strict_types=1);
namespace OCA\EcloudAccounts\Listeners;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Db\TwoFactorMapper;
use OCA\EcloudAccounts\Cron\TwoFactorStateChangeJob;
use OCA\EcloudAccounts\Service\SSOService;
use OCA\TwoFactorTOTP\Event\StateChanged;
use OCP\App\IAppManager;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\ILogger;

class TwoFactorStateChangedListener implements IEventListener {
	private IAppManager $appManager;
	private TwoFactorMapper $twoFactorMapper;
	private SSOService $ssoService;
	private IJobList $jobList;
	private ILogger $logger;

	private const TWOFACTOR_APP_ID = 'twofactor_totp';


	public function __construct(IAppManager $appManager, SSOService $ssoService, TwoFactorMapper $twoFactorMapper, ILogger $logger) {
	public function __construct(IAppManager $appManager, IJobList $jobList, SSOService $ssoService, ILogger $logger) {
		$this->appManager = $appManager;
		$this->ssoService = $ssoService;
		$this->twoFactorMapper = $twoFactorMapper;
		$this->jobList = $jobList;
		$this->logger = $logger;
	}

@@ -37,18 +37,19 @@ class TwoFactorStateChangedListener implements IEventListener {

		$user = $event->getUser();
		$username = $user->getUID();
		$enabled = $event->isEnabled();
		try {
			// When state change event is fired by user disabling 2FA, delete existing 2FA credentials and return
			// i.e. disable 2FA for user at SSO
			if (!$event->isEnabled()) {
				$this->ssoService->deleteCredentials($username);
				return;
			}

			$secret = $this->twoFactorMapper->getSecret($username);
			$this->ssoService->migrateCredential($username, $secret);
			$this->ssoService->handle2FAStateChange($enabled, $username);
		} catch (Exception $e) {
			$this->logger->logException($e, ['app' => Application::APP_ID]);

			// faced exception. Initiate BG job to retry again.
			$arguments = [
				TwoFactorStateChangeJob::ENABLED_KEY => $event->isEnabled(),
				TwoFactorStateChangeJob::USERNAME_KEY => $username,
				TwoFactorStateChangeJob::TRYCOUNT_KEY => 0
			];
			$this->jobList->add(TwoFactorStateChangeJob::class, $arguments);
		}
	}
}
+32 −5
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
namespace OCA\EcloudAccounts\Service;

use OCA\EcloudAccounts\AppInfo\Application;
use OCA\EcloudAccounts\Db\TwoFactorMapper;
use OCA\EcloudAccounts\Exception\SSOAdminAccessTokenException;
use OCA\EcloudAccounts\Exception\SSOAdminAPIException;
use OCP\IConfig;
@@ -25,6 +26,7 @@ class SSOService {
	private ICrypto $crypto;
	private IFactory $l10nFactory;
	private IUserManager $userManager;
	private TwoFactorMapper $twoFactorMapper;

	private string $mainDomain;
	private string $legacyDomain;
@@ -33,7 +35,7 @@ class SSOService {
	private const USERS_ENDPOINT = '/users';
	private const CREDENTIALS_ENDPOINT = '/users/{USER_ID}/credentials';

	public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, ILogger $logger) {
	public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, IUserManager $userManager, TwoFactorMapper $twoFactorMapper, ILogger $logger) {
		$this->appName = $appName;
		$this->config = $config;

@@ -53,6 +55,7 @@ class SSOService {
		$this->logger = $logger;
		$this->l10nFactory = $l10nFactory;
		$this->userManager = $userManager;
		$this->twoFactorMapper = $twoFactorMapper;

		$this->mainDomain = $this->config->getSystemValue("main_domain");
		$this->legacyDomain = $this->config->getSystemValue("legacy_domain");
@@ -64,7 +67,7 @@ class SSOService {

	public function migrateCredential(string $username, string $secret) : void {
		if($this->isNotCurrentUser($username)) {
			$this->getUserId($username);
			$this->setupUserId($username);
		}

		$this->deleteCredentials($username);
@@ -84,7 +87,7 @@ class SSOService {

	public function deleteCredentials(string $username) : void {
		if($this->isNotCurrentUser($username)) {
			$this->getUserId($username);
			$this->setupUserId($username);
		}

		$credentialIds = $this->getCredentialIds();
@@ -100,7 +103,7 @@ class SSOService {

	public function logout(string $username) : void {
		if($this->isNotCurrentUser($username)) {
			$this->getUserId($username);
			$this->setupUserId($username);
		}

		$url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId . '/logout';
@@ -109,6 +112,18 @@ class SSOService {
		$this->callSSOAPI($url, 'POST', [], 204);
	}

	public function handle2FAStateChange(bool $enabled, string $username) : void {
		// When state change event is fired by user disabling 2FA, delete existing 2FA credentials and return
		// i.e. disable 2FA for user at SSO
		if (!$enabled) {
			$this->deleteCredentials($username);
			return;
		}

		$secret = $this->twoFactorMapper->getSecret($username);
		$this->migrateCredential($username, $secret);
	}

	private function getCredentialIds() : array {
		$url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT;
		$url = str_replace('{USER_ID}', $this->currentUserId, $url);
@@ -159,7 +174,19 @@ class SSOService {
		return $credentialEntry;
	}

	private function getUserId(string $username) : void {
	private function setupUserId(string $username) : void {
		$user = $this->userManager->get($username);
		$savedOIDCUid = $this->config->getUserValue($user->getUID(), 'oidc_login', 'oidc_uid');

		if ($savedOIDCUid !== null && trim($savedOIDCUid) !== '') {
			$this->currentUserId = $savedOIDCUid;
			return;
		}

		$this->retrieveUserId($username);
	}

	private function retrieveUserId(string $username) {
		$user = $this->userManager->get($username);
		if ($user === null) {
			throw new SSOAdminAPIException('Error: no user exists in cloud with username ' . $username);