From 4d87b0c7b994d3c9b340f0a0c871b0ce8ceaa2c5 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 18 Apr 2024 19:31:14 +0530 Subject: [PATCH 01/21] Add API calls for delete/update creds --- lib/Service/SSOService.php | 203 +++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 lib/Service/SSOService.php diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php new file mode 100644 index 00000000..7d328f1c --- /dev/null +++ b/lib/Service/SSOService.php @@ -0,0 +1,203 @@ +appName = $appName; + $this->config = $config; + $this->twoFactorMapper = $twoFactorMapper; + $this->ssoConfig['admin_client_id'] = $this->config->getSystemValue('oidc_admin_client_id', ''); + $this->ssoConfig['admin_client_secret'] = $this->config->getSystemValue('oidc_admin_client_secret', ''); + $this->ssoConfig['admin_username'] = $this->config->getSystemValue('oidc_admin_username', ''); + $this->ssoConfig['admin_password'] = $this->config->getSystemValue('oidc_admin_password', ''); + $this->ssoConfig['provider_url'] = $this->config->getSystemValue('oidc_login_provider_url', ''); + $this->ssoConfig['root_url'] = explode('/auth', $this->ssoConfig['provider_url'])[0]; + $this->crypto = $crypto; + $this->curl = $curlService; + $this->logger = $logger; + } + + public function migrateCredential(string $username) : void { + if(empty($this->currentUserId)) { + $this->getUserId($username); + } + $this->deleteCredentials($username); + + $secret = $this->twoFactorMapper->getSecret($username); + $decryptedSecret = $this->crypto->decrypt($secret); + $language = $this->config->getUserValue($username, 'core', 'lang', 'en'); + $credentialEntry = $this->getCredentialEntry($decryptedSecret, $language); + $url = $this->ssoConfig['provider_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId; + + $data = [ + 'credentials' => [$credentialEntry] + ]; + $this->callSSOAPI($url, 'PUT', $data, 204); + } + + public function deleteCredentials(string $username) : void { + if(empty($this->currentUserId)) { + $this->getUserId($username); + } + $credentialIds = $this->getCredentialIds(); + + foreach ($credentialIds as $credentialId) { + $url = $this->ssoConfig['provider_url'] . self::CREDENTIALS_ENDPOINT; + $url = str_replace($url, '{USER_ID}', $this->currentUserId); + $url .= '/' . $credentialId; + + $this->callSSOAPI($url, 'DELETE', [], 204); + } + } + + private function getCredentialIds() : array { + $url = $this->ssoConfig['provider_url'] . self::CREDENTIALS_ENDPOINT; + $url = str_replace($url, '{USER_ID}', $this->currentUserId); + $credentials = $this->callSSOAPI($url, 'GET'); + + if (empty($credentials) || !is_array($credentials)) { + return []; + } + + $credentials = array_filter($credentials, function($credential) { + if ($credential['type'] !== 'otp') { + return false; + } + $credentialData = json_decode($credential['credentialData'], true); + if ($credentialData['subType'] !== 'totp' || $credentialData['secretEncoding'] !== 'BASE32') { + return false; + } + return true; + }); + + return array_map(function($credential) { + return $credential['id']; + }, $credentials); + } + + /** + * Create secret entry compatible with Keycloak schema + * + * @return array + */ + + private function getCredentialEntry(string $secret, string $language) : array { + $l10n = $this->l10nFactory->get(Application::APP_ID, $language); + $userLabel = $l10n->t('Murena Cloud 2FA'); + + $credentialEntry = [ + 'userLabel' => $userLabel, + 'type' => 'otp', + 'secretData' => json_encode([ + 'value' => $secret + ]), + 'credentialData' => json_encode([ + 'subType' => 'totp', + 'period' => 30, + 'digits' => 6, + 'algorithm' => 'HmacSHA1', + 'secretEncoding' => 'BASE32', + ]), + ]; + + foreach ($credentialEntry as $key => &$value) { + $value = "'" . $value . "'"; + } + return $credentialEntry; + } + + private function getUserId(string $username) : void { + $usernameWithoutDomain = explode('@', $username)[0]; + $url = $this->ssoConfig['provider_url'] . self::USERS_ENDPOINT . '?exact=true&username=' . $usernameWithoutDomain; + $users = $this->callSSOAPI($url, 'GET'); + if (empty($users) || !is_array($users) || !isset($users[0])) { + throw new Exception(); + } + $this->currentUserId = $users[0]['username']; + } + + private function getAdminAccessToken() : void { + if (!empty($this->adminAccessToken)) { + return; + } + $adminAccessTokenRoute = $this->ssoConfig['provider_url'] . self::ADMIN_TOKEN_ENDPOINT; + $requestBody = [ + 'username' => $this->ssoConfig['admin_username'], + 'password' => $this->ssoConfig['admin_password'], + 'client_id' => $this->ssoConfig['admin_client_id'], + 'client_secret' => $this->ssoConfig['admin_client_secret'], + 'grant_type' => 'password' + ]; + + $headers = [ + 'Content-Type: application/x-www-form-urlencoded' + ]; + $response = $this->curl->post($adminAccessTokenRoute, $requestBody, $headers); + + if (!$this->curl->getLastStatusCode() === 200) { + throw new Exception(); + } + $response = json_decode($response, true); + + if (!isset($response['access_token'])) { + throw new Exception(); + } + $this->adminAccessToken = $response['access_token']; + } + + private function callSSOAPI(string $url, string $method, array $data = [], int $expectedStatusCode = 200) :?array { + if (empty($url)) { + return null; + } + $accessToken = $this->getAdminAccessToken(); + $headers = [ + "cache-control: no-cache", + "content-type: application/json", + "Authorization: Bearer " . $accessToken + ]; + + if ($method === 'GET') { + $answer = $this->curl->get($shop['url'] . $endpoint, $data, $headers); + } + + if ($method === 'DELETE') { + $answer = $this->curl->delete($shop['url'] . $endpoint, $data, $headers); + } + + if ($method === 'POST') { + $answer = $this->curl->post($shop['url'] . $endpoint, json_encode($data), $headers); + } + + $statusCode = $this->curl->getLastStatusCode(); + + if ($statusCode !== $expectedStatusCode) { + throw new Exception(); + } + + $answer = json_decode($answer, true); + return $answer; + } +} -- GitLab From 769f9176ef9d2e1fb88b7dce290249a40f00d860 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 18 Apr 2024 19:37:41 +0530 Subject: [PATCH 02/21] Use SSO service instead of SSO mapper --- lib/Listeners/TwoFactorStateChangedListener.php | 14 +++++++------- lib/Service/SSOService.php | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index 1c93df33..d8c25223 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Listeners; use Exception; -use OCA\EcloudAccounts\Db\SSOMapper; +use OCA\EcloudAccounts\Service\SSOService; use OCA\EcloudAccounts\Db\TwoFactorMapper; use OCA\TwoFactorTOTP\Event\StateChanged; use OCP\App\IAppManager; @@ -16,22 +16,22 @@ use OCP\ILogger; class TwoFactorStateChangedListener implements IEventListener { private IAppManager $appManager; private TwoFactorMapper $twoFactorMapper; - private SSOMapper $ssoMapper; + private SSOService $ssoService; private ILogger $logger; private const TWOFACTOR_APP_ID = 'twofactor_totp'; - public function __construct(IAppManager $appManager, SSOMapper $ssoMapper, TwoFactorMapper $twoFactorMapper, ILogger $logger) { + public function __construct(IAppManager $appManager, SSOService $ssoService, TwoFactorMapper $twoFactorMapper, ILogger $logger) { $this->appManager = $appManager; - $this->ssoMapper = $ssoMapper; + $this->ssoService = $ssoService; $this->twoFactorMapper = $twoFactorMapper; $this->logger = $logger; } public function handle(Event $event): void { - if (!($event instanceof StateChanged) || !$this->appManager->isEnabledForUser(self::TWOFACTOR_APP_ID) || !$this->ssoMapper->isSSOEnabled()) { + if (!($event instanceof StateChanged) || !$this->appManager->isEnabledForUser(self::TWOFACTOR_APP_ID) || !$this->ssoService->shouldSync2FA()) { return; } @@ -41,12 +41,12 @@ class TwoFactorStateChangedListener implements IEventListener { // 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->ssoMapper->deleteCredentials($username); + $this->ssoService->deleteCredentials($username); return; } $secret = $this->twoFactorMapper->getSecret($username); - $this->ssoMapper->migrateCredential($username, $secret); + $this->ssoService->migrateCredential($username, $secret); } catch (Exception $e) { $stateText = $event->isEnabled() ? 'new secret enabled' : 'disabled'; $this->logger->error('Error updating secret state(' . $stateText .') for user: ' . $username . ': ' . $e->getMessage()); diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 7d328f1c..c19a41a9 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -5,7 +5,6 @@ namespace OCA\EcloudAccounts\Service; use Exception; use OCA\EcloudAccounts\AppInfo\Application; -use OCA\EcloudAccounts\Db\TwoFactorMapper; use OCP\IConfig; use OCP\ILogger; use OCP\Security\ICrypto; @@ -18,17 +17,15 @@ class SSOService { private array $ssoConfig = []; private string $adminAccessToken; private string $currentUserId; - private TwoFactorMapper $twoFactorMapper; private ICrypto $crypto; 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'; - public function __construct($appName, IConfig $config, CurlService $curlService, TwoFactorMapper $twoFactorMapper, ICrypto $crypto, ILogger $logger) { + public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, ILogger $logger) { $this->appName = $appName; $this->config = $config; - $this->twoFactorMapper = $twoFactorMapper; $this->ssoConfig['admin_client_id'] = $this->config->getSystemValue('oidc_admin_client_id', ''); $this->ssoConfig['admin_client_secret'] = $this->config->getSystemValue('oidc_admin_client_secret', ''); $this->ssoConfig['admin_username'] = $this->config->getSystemValue('oidc_admin_username', ''); @@ -40,13 +37,16 @@ class SSOService { $this->logger = $logger; } - public function migrateCredential(string $username) : void { + public function shouldSync2FA() : bool { + return $this->config->getSystemValue('oidc_admin_sync_2fa', false); + } + + public function migrateCredential(string $username, string $secret) : void { if(empty($this->currentUserId)) { $this->getUserId($username); } $this->deleteCredentials($username); - $secret = $this->twoFactorMapper->getSecret($username); $decryptedSecret = $this->crypto->decrypt($secret); $language = $this->config->getUserValue($username, 'core', 'lang', 'en'); $credentialEntry = $this->getCredentialEntry($decryptedSecret, $language); -- GitLab From d259cd69f3e6904f7b676f8718fd6244d23d2ab7 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 18 Apr 2024 19:38:08 +0530 Subject: [PATCH 03/21] Remove SSO Mapper --- lib/Db/SSOMapper.php | 193 ------------------------------------------- 1 file changed, 193 deletions(-) delete mode 100644 lib/Db/SSOMapper.php diff --git a/lib/Db/SSOMapper.php b/lib/Db/SSOMapper.php deleted file mode 100644 index 89f4af88..00000000 --- a/lib/Db/SSOMapper.php +++ /dev/null @@ -1,193 +0,0 @@ -l10nFactory = $l10nFactory; - $this->config = $config; - $this->logger = $logger; - $this->userManager = $userManager; - $this->crypto = $crypto; - if (!empty($this->config->getSystemValue(self::SSO_CONFIG_KEY))) { - $this->initConnection(); - } - } - - public function isSSOEnabled() : bool { - return isset($this->conn); - } - - public function getUserId(string $username) : string { - $qb = $this->conn->createQueryBuilder(); - $qb->select('USER_ID') - ->from(self::USER_ATTRIBUTE_TABLE) - ->where('NAME = "LDAP_ID"') - ->andWhere('VALUE = :username'); - - $qb->setParameter('username', $username); - $result = $qb->execute(); - return (string) $result->fetchOne(); - } - - public function deleteCredentials(string $username) { - $userId = $this->getUserId($username); - $qb = $this->conn->createQueryBuilder(); - $qb->delete(self::CREDENTIAL_TABLE) - ->where('USER_ID = :username') - ->andWhere('TYPE = "otp"') - ->andWhere('CREDENTIAL_DATA LIKE "%\"subType\":\"totp\"%"') - ->setParameter('username', $userId) - ->execute(); - } - - public function migrateCredential(string $username, string $secret) { - if (!$this->userManager->get($username) instanceof IUser) { - throw new \Exception('No user found in nextcloud with given username'); - } - - $decryptedSecret = $this->crypto->decrypt($secret); - $ssoUserId = $this->getUserId($username); - if (empty($ssoUserId)) { - throw new \Exception('Does not exist in SSO database'); - } - - $language = $this->config->getUserValue($username, 'core', 'lang', 'en'); - - // Only one 2FA device at a time - $this->deleteCredentials($username); - - $entry = $this->getCredentialEntry($decryptedSecret, $ssoUserId, $language); - $this->insertCredential($entry); - } - - public function insertCredential(array $entry) : void { - $qb = $this->conn->createQueryBuilder(); - $qb->insert(self::CREDENTIAL_TABLE) - ->values($entry) - ->execute(); - } - - /** - * Create secret entry compatible with Keycloak schema - * - * @return array - */ - - private function getCredentialEntry(string $secret, string $ssoUserId, string $language) : array { - // Create the random UUID from the sso user ID so multiple entries of same credential do not happen - $id = $this->randomUUID(substr($ssoUserId, 0, 16)); - - $l10n = $this->l10nFactory->get(Application::APP_ID, $language); - $userLabel = $l10n->t('Murena Cloud 2FA'); - - $credentialEntry = [ - 'ID' => $id, - 'USER_ID' => $ssoUserId, - 'USER_LABEL' => $userLabel, - 'TYPE' => 'otp', - 'SECRET_DATA' => json_encode([ - 'value' => $secret - ]), - 'CREDENTIAL_DATA' => json_encode([ - 'subType' => 'totp', - 'period' => 30, - 'digits' => 6, - 'algorithm' => 'HmacSHA1', - 'secretEncoding' => 'BASE32', - ]), - ]; - - foreach ($credentialEntry as $key => &$value) { - $value = "'" . $value . "'"; - } - $credentialEntry['CREATED_DATE'] = round(microtime(true) * 1000); - $credentialEntry['PRIORITY'] = 10; - - return $credentialEntry; - } - - private function initConnection() : void { - try { - $params = $this->getConnectionParams(); - $this->conn = DriverManager::getConnection($params); - } catch (Throwable $e) { - $this->logger->error('Error connecting to Keycloak database: ' . $e->getMessage()); - } - } - - private function isDbConfigValid($config) : bool { - if (!$config || !is_array($config)) { - return false; - } - if (!isset($config['db_port'])) { - $config['db_port'] = 3306; - } - - return isset($config['db_name']) - && isset($config['db_user']) - && isset($config['db_password']) - && isset($config['db_host']) - && isset($config['db_port']) ; - } - - private function getConnectionParams() : array { - $config = $this->config->getSystemValue(self::SSO_CONFIG_KEY); - - if (!$this->isDbConfigValid($config)) { - throw new DbConnectionParamsException('Invalid SSO database configuration!'); - } - - $params = [ - 'dbname' => $config['db_name'], - 'user' => $config['db_user'], - 'password' => $config['db_password'], - 'host' => $config['db_host'], - 'port' => $config['db_port'], - 'driver' => 'pdo_mysql' - ]; - return $params; - } - - /** - * From https://www.uuidgenerator.net/dev-corner/php - * As keycloak generates random UUIDs using the java.util.UUID class which is RFC 4122 compliant - * - * @return string - */ - private function randomUUID($data = null) : string { - // Generate 16 bytes (128 bits) of random data or use the data passed into the function. - $data = $data ?? random_bytes(16); - assert(strlen($data) == 16); - - // Set version to 0100 - $data[6] = chr(ord($data[6]) & 0x0f | 0x40); - // Set bits 6-7 to 10 - $data[8] = chr(ord($data[8]) & 0x3f | 0x80); - - // Output the 36 character UUID. - return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); - } -} -- GitLab From 9f6374711d4e75a35081468d4299ff52ba9e08fb Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 18 Apr 2024 19:39:25 +0530 Subject: [PATCH 04/21] Also fix occ command to use sso service --- lib/Command/Migrate2FASecrets.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index 79228be2..f741f5e0 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Command; -use OCA\EcloudAccounts\Db\SSOMapper; +use OCA\EcloudAccounts\Db\SSOService; use OCA\EcloudAccounts\Db\TwoFactorMapper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -12,12 +12,12 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Migrate2FASecrets extends Command { - private SSOMapper $ssoMapper; + private SSOService $ssoService; private TwoFactorMapper $twoFactorMapper; private OutputInterface $commandOutput; - public function __construct(SSOMapper $ssoMapper, TwoFactorMapper $twoFactorMapper) { - $this->ssoMapper = $ssoMapper; + public function __construct(SSOService $ssoService, TwoFactorMapper $twoFactorMapper) { + $this->ssoService = $ssoService; $this->twoFactorMapper = $twoFactorMapper; parent::__construct(); } @@ -60,7 +60,7 @@ class Migrate2FASecrets extends Command { $entries = $this->twoFactorMapper->getEntries($usernames); foreach ($entries as $entry) { try { - $this->ssoMapper->migrateCredential($entry['username'], $entry['secret']); + $this->ssoService->migrateCredential($entry['username'], $entry['secret']); } catch (\Exception $e) { $this->commandOutput->writeln('Error inserting entry for user ' . $entry['username'] . ' message: ' . $e->getMessage()); continue; -- GitLab From 2b03acadbfc5619c605c1f23e9ccd03defa70b16 Mon Sep 17 00:00:00 2001 From: Akhil Date: Thu, 18 Apr 2024 19:39:41 +0530 Subject: [PATCH 05/21] Run PHP linter --- lib/Listeners/TwoFactorStateChangedListener.php | 2 +- lib/Service/SSOService.php | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index d8c25223..a40ee3d2 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Listeners; use Exception; -use OCA\EcloudAccounts\Service\SSOService; use OCA\EcloudAccounts\Db\TwoFactorMapper; +use OCA\EcloudAccounts\Service\SSOService; use OCA\TwoFactorTOTP\Event\StateChanged; use OCP\App\IAppManager; use OCP\EventDispatcher\Event; diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index c19a41a9..038b2586 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -82,7 +82,7 @@ class SSOService { return []; } - $credentials = array_filter($credentials, function($credential) { + $credentials = array_filter($credentials, function ($credential) { if ($credential['type'] !== 'otp') { return false; } @@ -93,7 +93,7 @@ class SSOService { return true; }); - return array_map(function($credential) { + return array_map(function ($credential) { return $credential['id']; }, $credentials); } @@ -104,7 +104,7 @@ class SSOService { * @return array */ - private function getCredentialEntry(string $secret, string $language) : array { + private function getCredentialEntry(string $secret, string $language) : array { $l10n = $this->l10nFactory->get(Application::APP_ID, $language); $userLabel = $l10n->t('Murena Cloud 2FA'); -- GitLab From 8dd716a40a51bd5f235231607b5e52f10783b966 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 12:26:37 +0530 Subject: [PATCH 06/21] Fix url to call api --- lib/Service/SSOService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 038b2586..4f9d065f 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -180,15 +180,15 @@ class SSOService { ]; if ($method === 'GET') { - $answer = $this->curl->get($shop['url'] . $endpoint, $data, $headers); + $answer = $this->curl->get($url, $data, $headers); } if ($method === 'DELETE') { - $answer = $this->curl->delete($shop['url'] . $endpoint, $data, $headers); + $answer = $this->curl->delete($url, $data, $headers); } if ($method === 'POST') { - $answer = $this->curl->post($shop['url'] . $endpoint, json_encode($data), $headers); + $answer = $this->curl->post($url, json_encode($data), $headers); } $statusCode = $this->curl->getLastStatusCode(); -- GitLab From 56eec83168a06106a1de5c3a938dd3b6203774a6 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 13:17:45 +0530 Subject: [PATCH 07/21] Fix URLs in SSO service; fix curlservice to build post data --- lib/Service/CurlService.php | 27 +++++++++++++++++++-------- lib/Service/SSOService.php | 12 +++++++++--- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index 9cf95068..bcdc627b 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -54,6 +54,24 @@ class CurlService { return $this->lastStatusCode; } + private function buildPostData(array $params = [], array $headers = []) : string { + $jsonContent = in_array('Content-Type: application/json', $headers); + if ($jsonContent) { + $params = json_encode($params); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new Exception('JSON encoding failed: ' . json_last_error_msg()); + } + return $params; + } + + $formContent = in_array('Content-Type: application/x-www-form-urlencoded', $headers); + if ($formContent) { + $params = http_build_query($params); + return $params; + } + + return ''; + } /** * Curl run request @@ -82,14 +100,7 @@ class CurlService { break; case 'POST': $options[CURLOPT_POST] = true; - $jsonContent = in_array('Content-Type: application/json', $headers); - if ($jsonContent) { - $params = json_encode($params); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception('JSON encoding failed: ' . json_last_error_msg()); - } - } - $options[CURLOPT_POSTFIELDS] = $params; + $options[CURLOPT_POSTFIELDS] = $this->buildPostData($params, $headers); break; case 'DELETE': $options[CURLOPT_CUSTOMREQUEST] = "DELETE"; diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 4f9d065f..806714a5 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -26,12 +26,18 @@ class SSOService { public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, ILogger $logger) { $this->appName = $appName; $this->config = $config; + + $ssoProviderUrl = $this->config->getSystemValue('oidc_login_provider_url', ''); + $ssoProviderUrlParts = explode($ssoProviderUrl, '/auth'); + $rootUrl = $ssoProviderUrlParts[0]; + $realmsPart = $ssoProviderUrl[1]; + $this->ssoConfig['admin_client_id'] = $this->config->getSystemValue('oidc_admin_client_id', ''); $this->ssoConfig['admin_client_secret'] = $this->config->getSystemValue('oidc_admin_client_secret', ''); $this->ssoConfig['admin_username'] = $this->config->getSystemValue('oidc_admin_username', ''); $this->ssoConfig['admin_password'] = $this->config->getSystemValue('oidc_admin_password', ''); - $this->ssoConfig['provider_url'] = $this->config->getSystemValue('oidc_login_provider_url', ''); - $this->ssoConfig['root_url'] = explode('/auth', $this->ssoConfig['provider_url'])[0]; + $this->ssoConfig['admin_rest_api_url'] = $rootUrl . '/auth/admin' . $realmsPart; + $this->ssoConfig['root_url'] = $rootUrl; $this->crypto = $crypto; $this->curl = $curlService; $this->logger = $logger; @@ -143,7 +149,7 @@ class SSOService { if (!empty($this->adminAccessToken)) { return; } - $adminAccessTokenRoute = $this->ssoConfig['provider_url'] . self::ADMIN_TOKEN_ENDPOINT; + $adminAccessTokenRoute = $this->ssoConfig['root_url'] . self::ADMIN_TOKEN_ENDPOINT; $requestBody = [ 'username' => $this->ssoConfig['admin_username'], 'password' => $this->ssoConfig['admin_password'], -- GitLab From 234f7675a4c724e871c09f8f3b79dbb4b241028c Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 17:25:16 +0530 Subject: [PATCH 08/21] add l10nfactory; add PUT to curlservice --- lib/Service/CurlService.php | 11 ++++++++-- lib/Service/SSOService.php | 43 ++++++++++++++++++++++++------------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index bcdc627b..7c966460 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -46,6 +46,10 @@ class CurlService { return $this->request('DELETE', $url, $params, $headers, $userOptions); } + public function put($url, $params = [], $headers = [], $userOptions = []) { + return $this->request('PUT', $url, $params, $headers, $userOptions); + } + /** * @return int */ @@ -54,7 +58,7 @@ class CurlService { return $this->lastStatusCode; } - private function buildPostData(array $params = [], array $headers = []) : string { + private function buildPostData($params = [], $headers = []) { $jsonContent = in_array('Content-Type: application/json', $headers); if ($jsonContent) { $params = json_encode($params); @@ -70,7 +74,7 @@ class CurlService { return $params; } - return ''; + return $params; } /** @@ -108,6 +112,9 @@ class CurlService { $url = $url . '?' . http_build_query($params); } break; + case 'PUT': + $options[CURLOPT_CUSTOMREQUEST] = "PUT"; + $options[CURLOPT_POSTFIELDS] = $this->buildPostData($params, $headers); default: throw new Exception('Unsuported method.'); break; diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 806714a5..033d0a2f 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -8,6 +8,7 @@ use OCA\EcloudAccounts\AppInfo\Application; use OCP\IConfig; use OCP\ILogger; use OCP\Security\ICrypto; +use OCP\L10N\IFactory; class SSOService { private IConfig $config; @@ -18,19 +19,20 @@ class SSOService { private string $adminAccessToken; private string $currentUserId; private ICrypto $crypto; + private IFactory $l10nFactory; 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'; - public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, ILogger $logger) { + public function __construct($appName, IConfig $config, CurlService $curlService, ICrypto $crypto, IFactory $l10nFactory, ILogger $logger) { $this->appName = $appName; $this->config = $config; $ssoProviderUrl = $this->config->getSystemValue('oidc_login_provider_url', ''); - $ssoProviderUrlParts = explode($ssoProviderUrl, '/auth'); + $ssoProviderUrlParts = explode('/auth', $ssoProviderUrl); $rootUrl = $ssoProviderUrlParts[0]; - $realmsPart = $ssoProviderUrl[1]; + $realmsPart = $ssoProviderUrlParts[1]; $this->ssoConfig['admin_client_id'] = $this->config->getSystemValue('oidc_admin_client_id', ''); $this->ssoConfig['admin_client_secret'] = $this->config->getSystemValue('oidc_admin_client_secret', ''); @@ -41,6 +43,7 @@ class SSOService { $this->crypto = $crypto; $this->curl = $curlService; $this->logger = $logger; + $this->l10nFactory = $l10nFactory; } public function shouldSync2FA() : bool { @@ -56,11 +59,13 @@ class SSOService { $decryptedSecret = $this->crypto->decrypt($secret); $language = $this->config->getUserValue($username, 'core', 'lang', 'en'); $credentialEntry = $this->getCredentialEntry($decryptedSecret, $language); - $url = $this->ssoConfig['provider_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId; + $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '/' . $this->currentUserId; $data = [ 'credentials' => [$credentialEntry] ]; + + $this->logger->debug('migrateCredential calling SSO API with url: '. $url . ' and data: ' . print_r($data, true)); $this->callSSOAPI($url, 'PUT', $data, 204); } @@ -71,17 +76,19 @@ class SSOService { $credentialIds = $this->getCredentialIds(); foreach ($credentialIds as $credentialId) { - $url = $this->ssoConfig['provider_url'] . self::CREDENTIALS_ENDPOINT; - $url = str_replace($url, '{USER_ID}', $this->currentUserId); + $url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT; + $url = str_replace('{USER_ID}', $this->currentUserId, $url); $url .= '/' . $credentialId; - + $this->logger->debug('deleteCredentials calling SSO API with url: '. $url); $this->callSSOAPI($url, 'DELETE', [], 204); } } private function getCredentialIds() : array { - $url = $this->ssoConfig['provider_url'] . self::CREDENTIALS_ENDPOINT; - $url = str_replace($url, '{USER_ID}', $this->currentUserId); + $url = $this->ssoConfig['admin_rest_api_url'] . self::CREDENTIALS_ENDPOINT; + $url = str_replace('{USER_ID}', $this->currentUserId, $url); + $this->logger->debug('getCredentialIds calling SSO API with url: '. $url); + $credentials = $this->callSSOAPI($url, 'GET'); if (empty($credentials) || !is_array($credentials)) { @@ -137,12 +144,13 @@ class SSOService { private function getUserId(string $username) : void { $usernameWithoutDomain = explode('@', $username)[0]; - $url = $this->ssoConfig['provider_url'] . self::USERS_ENDPOINT . '?exact=true&username=' . $usernameWithoutDomain; + $url = $this->ssoConfig['admin_rest_api_url'] . self::USERS_ENDPOINT . '?exact=true&username=' . $usernameWithoutDomain; + $this->logger->debug('getUserId calling SSO API with url: '. $url); $users = $this->callSSOAPI($url, 'GET'); if (empty($users) || !is_array($users) || !isset($users[0])) { throw new Exception(); } - $this->currentUserId = $users[0]['username']; + $this->currentUserId = $users[0]['id']; } private function getAdminAccessToken() : void { @@ -161,6 +169,7 @@ class SSOService { $headers = [ 'Content-Type: application/x-www-form-urlencoded' ]; + $this->logger->debug('getAdminAccessToken calling SSO API with url: '. $adminAccessTokenRoute . ' and headers: ' . print_r($headers, true) . ' and body: ' . print_r($requestBody, true)); $response = $this->curl->post($adminAccessTokenRoute, $requestBody, $headers); if (!$this->curl->getLastStatusCode() === 200) { @@ -178,11 +187,11 @@ class SSOService { if (empty($url)) { return null; } - $accessToken = $this->getAdminAccessToken(); + $this->getAdminAccessToken(); $headers = [ "cache-control: no-cache", - "content-type: application/json", - "Authorization: Bearer " . $accessToken + "Content-Type: application/json", + "Authorization: Bearer " . $this->adminAccessToken ]; if ($method === 'GET') { @@ -194,7 +203,11 @@ class SSOService { } if ($method === 'POST') { - $answer = $this->curl->post($url, json_encode($data), $headers); + $answer = $this->curl->post($url, $data, $headers); + } + + if ($method === 'PUT') { + $answer = $this->curl->put($url, $data, $headers); } $statusCode = $this->curl->getLastStatusCode(); -- GitLab From 1f327d3bb12a30a330247cb2d4c23a667a4ab348 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 17:26:33 +0530 Subject: [PATCH 09/21] do a lint check --- lib/Service/CurlService.php | 1 + lib/Service/SSOService.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index 7c966460..5260ec46 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -115,6 +115,7 @@ class CurlService { case 'PUT': $options[CURLOPT_CUSTOMREQUEST] = "PUT"; $options[CURLOPT_POSTFIELDS] = $this->buildPostData($params, $headers); + // no break default: throw new Exception('Unsuported method.'); break; diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 033d0a2f..d582a8af 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -7,8 +7,8 @@ use Exception; use OCA\EcloudAccounts\AppInfo\Application; use OCP\IConfig; use OCP\ILogger; -use OCP\Security\ICrypto; use OCP\L10N\IFactory; +use OCP\Security\ICrypto; class SSOService { private IConfig $config; -- GitLab From fad2e6e1dc11f9cd08c77da28a7fa672e8351275 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 17:32:22 +0530 Subject: [PATCH 10/21] Add temporary staging deploy --- .gitlab-ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c268fb41..62a33242 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,3 +21,22 @@ build-vendor: artifacts: paths: - dist/ + + +deploy:staging: + extends: .deploy:nextcloud-app + rules: + - if: $CI_COMMIT_BRANCH == "main" + when: manual + - if: $CI_COMMIT_BRANCH == "murena-main" + when: manual + - if: $CI_COMMIT_BRANCH == "production" + when: manual + - if: $CI_COMMIT_BRANCH == "dev/2fa-kc-api" + when: manual + - if: $CI_COMMIT_TAG + when: manual + environment: + name: staging/01 + url: $ENV_URL + -- GitLab From e27014519f435d82094c1cb554dcac3b0cf00839 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 17:37:28 +0530 Subject: [PATCH 11/21] Fix namespace of SSOService --- lib/Command/Migrate2FASecrets.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index f741f5e0..f1eedb4c 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -4,7 +4,7 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Command; -use OCA\EcloudAccounts\Db\SSOService; +use OCA\EcloudAccounts\Service\SSOService; use OCA\EcloudAccounts\Db\TwoFactorMapper; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; -- GitLab From 724a337e2f557a2c7e8dbd5fcb72cb323b849c82 Mon Sep 17 00:00:00 2001 From: Akhil Date: Mon, 22 Apr 2024 18:52:54 +0530 Subject: [PATCH 12/21] Fix json encoding of credential PUT body --- lib/Command/Migrate2FASecrets.php | 2 +- lib/Service/CurlService.php | 4 ++-- lib/Service/SSOService.php | 18 ++++-------------- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/Command/Migrate2FASecrets.php b/lib/Command/Migrate2FASecrets.php index f1eedb4c..69683768 100644 --- a/lib/Command/Migrate2FASecrets.php +++ b/lib/Command/Migrate2FASecrets.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Command; -use OCA\EcloudAccounts\Service\SSOService; use OCA\EcloudAccounts\Db\TwoFactorMapper; +use OCA\EcloudAccounts\Service\SSOService; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index 5260ec46..4e226bab 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -115,9 +115,9 @@ class CurlService { case 'PUT': $options[CURLOPT_CUSTOMREQUEST] = "PUT"; $options[CURLOPT_POSTFIELDS] = $this->buildPostData($params, $headers); - // no break + break; default: - throw new Exception('Unsuported method.'); + throw new Exception('Unsupported method.'); break; } $options[CURLOPT_URL] = $url; diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index d582a8af..2d538846 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -121,24 +121,14 @@ class SSOService { $l10n = $this->l10nFactory->get(Application::APP_ID, $language); $userLabel = $l10n->t('Murena Cloud 2FA'); + $secretData = '{"value":"' . $secret . '"}'; + $credentialData = '{"subType":"totp","period":30,"digits":6,"algorithm":"HmacSHA1","secretEncoding":"BASE32"}'; $credentialEntry = [ 'userLabel' => $userLabel, 'type' => 'otp', - 'secretData' => json_encode([ - 'value' => $secret - ]), - 'credentialData' => json_encode([ - 'subType' => 'totp', - 'period' => 30, - 'digits' => 6, - 'algorithm' => 'HmacSHA1', - 'secretEncoding' => 'BASE32', - ]), + 'secretData' => $secretData, + 'credentialData' => $credentialData ]; - - foreach ($credentialEntry as $key => &$value) { - $value = "'" . $value . "'"; - } return $credentialEntry; } -- GitLab From 52d7f370f457021c0d2c032c23cafc1c22f9035e Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 23 Apr 2024 20:29:01 +0530 Subject: [PATCH 13/21] Throw exceptions for errors --- lib/Exception/SSOAdminAPIException.php | 9 +++++++++ lib/Exception/SSOAdminAccessTokenException.php | 9 +++++++++ lib/Service/SSOService.php | 11 +++++++---- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 lib/Exception/SSOAdminAPIException.php create mode 100644 lib/Exception/SSOAdminAccessTokenException.php diff --git a/lib/Exception/SSOAdminAPIException.php b/lib/Exception/SSOAdminAPIException.php new file mode 100644 index 00000000..675dd687 --- /dev/null +++ b/lib/Exception/SSOAdminAPIException.php @@ -0,0 +1,9 @@ +logger->debug('getUserId calling SSO API with url: '. $url); $users = $this->callSSOAPI($url, 'GET'); if (empty($users) || !is_array($users) || !isset($users[0])) { - throw new Exception(); + throw new SSOAdminAPIException('Error: no user found for search with url: ' . $url); } $this->currentUserId = $users[0]['id']; } @@ -163,12 +165,13 @@ class SSOService { $response = $this->curl->post($adminAccessTokenRoute, $requestBody, $headers); if (!$this->curl->getLastStatusCode() === 200) { - throw new Exception(); + $statusCode = strval($this->curl->getLastStatusCode()); + throw new SSOAdminAccessTokenException('Error getting Admin Access token. Status Code: ' . $statusCode); } $response = json_decode($response, true); if (!isset($response['access_token'])) { - throw new Exception(); + throw new SSOAdminAccessTokenException('Error: admin access token not set in response!'); } $this->adminAccessToken = $response['access_token']; } @@ -203,7 +206,7 @@ class SSOService { $statusCode = $this->curl->getLastStatusCode(); if ($statusCode !== $expectedStatusCode) { - throw new Exception(); + throw new SSOAdminAPIException('Error calling SSO API with url ' . $url . ' status code: ' . $statusCode); } $answer = json_decode($answer, true); -- GitLab From bd0c0641264acdb3cb661a25ab68225c80e3a787 Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 23 Apr 2024 20:32:51 +0530 Subject: [PATCH 14/21] Run php linter --- lib/Service/SSOService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index d43f8c2f..5d914960 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -3,7 +3,6 @@ namespace OCA\EcloudAccounts\Service; -use Exception; use OCA\EcloudAccounts\AppInfo\Application; use OCA\EcloudAccounts\Exception\SSOAdminAccessTokenException; use OCA\EcloudAccounts\Exception\SSOAdminAPIException; -- GitLab From b70fdef5a7d7cf00f80f8c6dc2c7c6790d445667 Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 23 Apr 2024 20:35:36 +0530 Subject: [PATCH 15/21] Don't catch exception to show error message to user --- .../TwoFactorStateChangedListener.php | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index a40ee3d2..7d50c709 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Listeners; -use Exception; use OCA\EcloudAccounts\Db\TwoFactorMapper; use OCA\EcloudAccounts\Service\SSOService; use OCA\TwoFactorTOTP\Event\StateChanged; @@ -37,19 +36,15 @@ class TwoFactorStateChangedListener implements IEventListener { $user = $event->getUser(); $username = $user->getUID(); - 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); - } catch (Exception $e) { - $stateText = $event->isEnabled() ? 'new secret enabled' : 'disabled'; - $this->logger->error('Error updating secret state(' . $stateText .') for user: ' . $username . ': ' . $e->getMessage()); + // 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); + } } -- GitLab From 31e84ce7419081e8747cf6e0cf8df9c518828b2c Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 23 Apr 2024 20:48:35 +0530 Subject: [PATCH 16/21] Re-add try-catch and use logException in listener --- .../TwoFactorStateChangedListener.php | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/Listeners/TwoFactorStateChangedListener.php b/lib/Listeners/TwoFactorStateChangedListener.php index 7d50c709..32308841 100644 --- a/lib/Listeners/TwoFactorStateChangedListener.php +++ b/lib/Listeners/TwoFactorStateChangedListener.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace OCA\EcloudAccounts\Listeners; +use OCA\EcloudAccounts\AppInfo\Application; use OCA\EcloudAccounts\Db\TwoFactorMapper; use OCA\EcloudAccounts\Service\SSOService; use OCA\TwoFactorTOTP\Event\StateChanged; @@ -36,15 +37,18 @@ class TwoFactorStateChangedListener implements IEventListener { $user = $event->getUser(); $username = $user->getUID(); - // 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; + 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); + } catch (Exception $e) { + $this->logger->logException($e, ['app' => Application::APP_ID]); } - - $secret = $this->twoFactorMapper->getSecret($username); - $this->ssoService->migrateCredential($username, $secret); - } } -- GitLab From 0fff97790fc3fe831c4e27a0b9687f0d55de87c7 Mon Sep 17 00:00:00 2001 From: AVINASH GUSAIN Date: Tue, 23 Apr 2024 16:06:53 +0000 Subject: [PATCH 17/21] Apply 1 suggestion(s) to 1 file(s) Co-authored-by: AVINASH GUSAIN --- lib/Service/CurlService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/CurlService.php b/lib/Service/CurlService.php index 4e226bab..a201e51b 100644 --- a/lib/Service/CurlService.php +++ b/lib/Service/CurlService.php @@ -95,7 +95,7 @@ class CurlService { CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers ); - array_merge($options, $userOptions); + $options = array_merge($options, $userOptions); switch ($method) { case 'GET': if ($params) { -- GitLab From 553be782b1b6d5330cea89911cdf32c514fbd76b Mon Sep 17 00:00:00 2001 From: Akhil Date: Tue, 23 Apr 2024 21:59:43 +0530 Subject: [PATCH 18/21] Fix conditional in getAdminAccessToken --- lib/Service/SSOService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 5d914960..59866c0c 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -163,7 +163,7 @@ class SSOService { $this->logger->debug('getAdminAccessToken calling SSO API with url: '. $adminAccessTokenRoute . ' and headers: ' . print_r($headers, true) . ' and body: ' . print_r($requestBody, true)); $response = $this->curl->post($adminAccessTokenRoute, $requestBody, $headers); - if (!$this->curl->getLastStatusCode() === 200) { + if ($this->curl->getLastStatusCode() !== 200) { $statusCode = strval($this->curl->getLastStatusCode()); throw new SSOAdminAccessTokenException('Error getting Admin Access token. Status Code: ' . $statusCode); } -- GitLab From 2e8d312846430781a0679703d858b4d1d5c43e29 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 24 Apr 2024 00:12:49 +0530 Subject: [PATCH 19/21] Check also if json data from api is set correctly --- lib/Service/SSOService.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Service/SSOService.php b/lib/Service/SSOService.php index 59866c0c..b176b59d 100644 --- a/lib/Service/SSOService.php +++ b/lib/Service/SSOService.php @@ -97,11 +97,12 @@ class SSOService { } $credentials = array_filter($credentials, function ($credential) { - if ($credential['type'] !== 'otp') { + if ($credential['type'] !== 'otp' || !isset($credential['credentialData'])) { return false; } $credentialData = json_decode($credential['credentialData'], true); - if ($credentialData['subType'] !== 'totp' || $credentialData['secretEncoding'] !== 'BASE32') { + if (!isset($credentialData['subType']) || !isset($credentialData['subType']) + || $credentialData['subType'] !== 'totp' || $credentialData['secretEncoding'] !== 'BASE32') { return false; } return true; @@ -122,6 +123,7 @@ class SSOService { $l10n = $this->l10nFactory->get(Application::APP_ID, $language); $userLabel = $l10n->t('Murena Cloud 2FA'); + // We build manually instead of json_encode to avoid any tricky escaping issues as this is a nested json object $secretData = '{"value":"' . $secret . '"}'; $credentialData = '{"subType":"totp","period":30,"digits":6,"algorithm":"HmacSHA1","secretEncoding":"BASE32"}'; $credentialEntry = [ -- GitLab From 6a9761e383e0ed1d3b1264d7ad1ab729a281e603 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 24 Apr 2024 00:16:18 +0530 Subject: [PATCH 20/21] Update README.md to document configuration changes --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index d5a0529d..42c9c93d 100644 --- a/README.md +++ b/README.md @@ -57,3 +57,14 @@ The values should be set as follows: 'welcome_sendgrid_template_ids' => [ 'en' => 'EN_TEMPLATE_ID', 'es' => 'ES_TEMPLATE_ID', ... ] ... ``` + +## Sync 2FA secrets with Keycloak based SSO service + +- Enable admin service client in Keycloak +- Configure the following parameters in `config.php`: + - `oidc_admin_client_id` (client ID of the admin service client) + - `oidc_admin_client_secret` (client secret of the admin service client) + - `oidc_admin_username` (username of admin account) + - `oidc_admin_password` (password of admin account) + - `oidc_login_provider_url` (provider URL: see also https://github.com/pulsejet/nextcloud-oidc-login) + - `oidc_admin_sync_2fa` -> (set to boolean value true to enable sync; defaults to false) -- GitLab From af560d101589b2f9a929f6ce21c6ac6422dbf4c5 Mon Sep 17 00:00:00 2001 From: Akhil Date: Wed, 24 Apr 2024 00:20:11 +0530 Subject: [PATCH 21/21] Remove gitlab CI changes; bump version --- .gitlab-ci.yml | 19 ------------------- appinfo/info.xml | 2 +- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 62a33242..c268fb41 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,22 +21,3 @@ build-vendor: artifacts: paths: - dist/ - - -deploy:staging: - extends: .deploy:nextcloud-app - rules: - - if: $CI_COMMIT_BRANCH == "main" - when: manual - - if: $CI_COMMIT_BRANCH == "murena-main" - when: manual - - if: $CI_COMMIT_BRANCH == "production" - when: manual - - if: $CI_COMMIT_BRANCH == "dev/2fa-kc-api" - when: manual - - if: $CI_COMMIT_TAG - when: manual - environment: - name: staging/01 - url: $ENV_URL - diff --git a/appinfo/info.xml b/appinfo/info.xml index 0d83ecbb..351006aa 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -10,7 +10,7 @@ - 5.0.0 + 5.1.0 agpl Murena SAS EcloudAccounts -- GitLab