diff --git a/lib/Controller/LoginController.php b/lib/Controller/LoginController.php index 657b18fa..598815be 100644 --- a/lib/Controller/LoginController.php +++ b/lib/Controller/LoginController.php @@ -25,7 +25,9 @@ use OCA\UserOIDC\Service\ProviderService; use OCA\UserOIDC\Service\ProvisioningService; use OCA\UserOIDC\Service\TokenService; +use OCA\UserOIDC\User\Backend; use OCA\UserOIDC\Vendor\Firebase\JWT\JWT; +use OCA\UserOIDC\Vendor\Firebase\JWT\Key; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Http; @@ -47,6 +49,8 @@ use OCP\Security\ICrypto; use OCP\Security\ISecureRandom; use OCP\Session\Exceptions\SessionNotAvailableException; +use OCP\User\Events\BeforeUserLoggedInEvent; +use OCP\User\Events\UserLoggedInEvent; use Psr\Log\LoggerInterface; class LoginController extends BaseOidcController { @@ -509,6 +513,9 @@ public function code(string $state = '', string $code = '', string $scope = '', $this->userSession->completeLogin($user, ['loginName' => $user->getUID(), 'password' => '']); $this->userSession->createSessionToken($this->request, $user->getUID(), $user->getUID()); $this->userSession->createRememberMeToken($user); + // TODO server should/could be refactored so we don't need to manually create the user session and dispatch the login-related events + $this->eventDispatcher->dispatchTyped(new BeforeUserLoggedInEvent($user->getUID(), null, \OC::$server->get(Backend::class))); + $this->eventDispatcher->dispatchTyped(new UserLoggedInEvent($user, $user->getUID(), null, false)); } // store all token information for potential token exchange requests @@ -554,6 +561,7 @@ public function code(string $state = '', string $code = '', string $scope = '', /** * Endpoint called by NC to logout in the IdP before killing the current session * + * @PublicPage * @NoAdminRequired * @NoCSRFRequired * @UseSession @@ -569,7 +577,23 @@ public function singleLogoutService() { $oidcSystemConfig = $this->config->getSystemValue('user_oidc', []); $targetUrl = $this->urlGenerator->getAbsoluteURL('/'); if (!isset($oidcSystemConfig['single_logout']) || $oidcSystemConfig['single_logout']) { - $providerId = $this->session->get(self::PROVIDERID); + $isFromGS = ($this->config->getSystemValueBool('gs.enabled', false) + && $this->config->getSystemValueString('gss.mode', '') === 'master'); + if ($isFromGS) { + // Request is from master GlobalScale: we get the provider ID from the JWT token provided by the slave + $jwt = $this->request->getParam('jwt', ''); + + try { + $key = $this->config->getSystemValueString('gss.jwt.key', ''); + $decoded = (array)JWT::decode($jwt, new Key($key, 'HS256')); + + $providerId = $decoded['oidcProviderId'] ?? null; + } catch (\Exception $e) { + $this->logger->debug('Failed to get the logout provider ID in the request from GSS', ['exception' => $e]); + } + } else { + $providerId = $this->session->get(self::PROVIDERID); + } if ($providerId) { try { $provider = $this->providerMapper->getProvider((int)$providerId); diff --git a/lib/Service/ProvisioningService.php b/lib/Service/ProvisioningService.php index 8d6ba9dc..e41769fc 100644 --- a/lib/Service/ProvisioningService.php +++ b/lib/Service/ProvisioningService.php @@ -19,6 +19,7 @@ use OCP\IConfig; use OCP\IGroupManager; use OCP\Image; +use OCP\ISession; use OCP\IUser; use OCP\IUserManager; use OCP\User\Events\UserChangedEvent; @@ -39,6 +40,7 @@ public function __construct( private IClientService $clientService, private IAvatarManager $avatarManager, private IConfig $config, + private ISession $session, ) { } @@ -60,6 +62,9 @@ public function hasOidcUserProvisitioned(string $userId): bool { * @throws Exception */ public function provisionUser(string $tokenUserId, int $providerId, object $idTokenPayload, ?IUser $existingLocalUser = null): ?IUser { + // user data potentially later used by globalsiteselector if user_oidc is used with global scale + $oidcGssUserData = get_object_vars($idTokenPayload); + // get name/email/quota information from the token itself $emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email'); $email = $idTokenPayload->{$emailAttribute} ?? null; @@ -157,6 +162,7 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $this->eventDispatcher->dispatchTyped($event); $this->logger->debug('Displayname mapping event dispatched'); if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') { + $oidcGssUserData[$displaynameAttribute] = $event->getValue(); $newDisplayName = $event->getValue(); if ($existingLocalUser === null) { $oldDisplayName = $backendUser->getDisplayName(); @@ -183,6 +189,7 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $this->eventDispatcher->dispatchTyped($event); $this->logger->debug('Email mapping event dispatched'); if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') { + $oidcGssUserData[$emailAttribute] = $event->getValue(); $user->setSystemEMailAddress($event->getValue()); } @@ -191,12 +198,21 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $this->eventDispatcher->dispatchTyped($event); $this->logger->debug('Quota mapping event dispatched'); if ($event->hasValue() && $event->getValue() !== null && $event->getValue() !== '') { + $oidcGssUserData[$quotaAttribute] = $event->getValue(); $user->setQuota($event->getValue()); } // Update groups if ($this->providerService->getSetting($providerId, ProviderService::SETTING_GROUP_PROVISIONING, '0') === '1') { - $this->provisionUserGroups($user, $providerId, $idTokenPayload); + $groups = $this->provisionUserGroups($user, $providerId, $idTokenPayload); + // for gss + if ($groups !== null) { + $groupIds = array_map(static function ($group) { + return $group->gid; + }, $groups); + $groupsAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_GROUPS, 'groups'); + $oidcGssUserData[$groupsAttribute] = $groupIds; + } } // Update the phone number @@ -318,6 +334,8 @@ public function provisionUser(string $tokenUserId, int $providerId, object $idTo $account->setProperty('gender', $event->getValue(), $scope, '1', ''); } + $this->session->set('user_oidc.oidcUserData', $oidcGssUserData); + $this->accountManager->updateAccount($account); return $user; } @@ -441,37 +459,41 @@ public function getSyncGroupsOfToken(int $providerId, object $idTokenPayload) { return null; } - public function provisionUserGroups(IUser $user, int $providerId, object $idTokenPayload): void { + public function provisionUserGroups(IUser $user, int $providerId, object $idTokenPayload): ?array { $groupsWhitelistRegex = $this->getGroupWhitelistRegex($providerId); $syncGroups = $this->getSyncGroupsOfToken($providerId, $idTokenPayload); - if ($syncGroups !== null) { + if ($syncGroups === null) { + return null; + } - $userGroups = $this->groupManager->getUserGroups($user); - foreach ($userGroups as $group) { - if (!in_array($group->getGID(), array_column($syncGroups, 'gid'))) { - if ($groupsWhitelistRegex && !preg_match($groupsWhitelistRegex, $group->getGID())) { - continue; - } - $group->removeUser($user); + $userGroups = $this->groupManager->getUserGroups($user); + foreach ($userGroups as $group) { + if (!in_array($group->getGID(), array_column($syncGroups, 'gid'))) { + if ($groupsWhitelistRegex && !preg_match($groupsWhitelistRegex, $group->getGID())) { + continue; } + $group->removeUser($user); } + } - foreach ($syncGroups as $group) { - // Creates a new group or return the exiting one. - if ($newGroup = $this->groupManager->createGroup($group->gid)) { - // Adds the user to the group. Does nothing if user is already in the group. - $newGroup->addUser($user); + foreach ($syncGroups as $group) { + // Creates a new group or return the exiting one. + if ($newGroup = $this->groupManager->createGroup($group->gid)) { + // Adds the user to the group. Does nothing if user is already in the group. + $newGroup->addUser($user); - if (isset($group->displayName)) { - $newGroup->setDisplayName($group->displayName); - } + if (isset($group->displayName)) { + $newGroup->setDisplayName($group->displayName); } } } + + return $syncGroups; } + public function getGroupWhitelistRegex(int $providerId): string { $regex = $this->providerService->getSetting($providerId, ProviderService::SETTING_GROUP_WHITELIST_REGEX, ''); diff --git a/lib/User/Backend.php b/lib/User/Backend.php index a33efc1c..ca49333e 100644 --- a/lib/User/Backend.php +++ b/lib/User/Backend.php @@ -149,6 +149,52 @@ public function getLogoutUrl(): string { ); } + /** + * Return user data from the idp + * Inspired by user_saml + */ + public function getUserData(): array { + $userData = $this->session->get('user_oidc.oidcUserData'); + $providerId = (int)$this->session->get(LoginController::PROVIDERID); + $userData = $this->formatUserData($providerId, $userData); + + // make sure that a valid UID is given + if (empty($userData['formatted']['uid'])) { + $this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', ['app' => 'user_oidc', 'uid' => $userData['formatted']['uid']]); + throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $userData['formatted']['uid']); + } + + return $userData; + } + + /** + * Format user data and map them to the configured attributes + * Inspired by user_saml + */ + private function formatUserData(int $providerId, array $attributes): array { + $result = ['formatted' => [], 'raw' => $attributes]; + + $emailAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_EMAIL, 'email'); + $result['formatted']['email'] = $attributes[$emailAttribute] ?? null; + + $displaynameAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_DISPLAYNAME, 'name'); + $result['formatted']['displayName'] = $attributes[$displaynameAttribute] ?? null; + + $quotaAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_QUOTA, 'quota'); + $result['formatted']['quota'] = $attributes[$quotaAttribute] ?? null; + if ($result['formatted']['quota'] === '') { + $result['formatted']['quota'] = 'default'; + } + + $groupsAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_GROUPS, 'groups'); + $result['formatted']['groups'] = $attributes[$groupsAttribute] ?? null; + + $uidAttribute = $this->providerService->getSetting($providerId, ProviderService::SETTING_MAPPING_UID, 'sub'); + $result['formatted']['uid'] = $attributes[$uidAttribute] ?? null; + + return $result; + } + /** * Return the id of the current user * @return string diff --git a/tests/unit/Service/ProvisioningServiceTest.php b/tests/unit/Service/ProvisioningServiceTest.php index 1c830434..b4db605b 100644 --- a/tests/unit/Service/ProvisioningServiceTest.php +++ b/tests/unit/Service/ProvisioningServiceTest.php @@ -18,6 +18,7 @@ use OCP\IConfig; use OCP\IGroup; use OCP\IGroupManager; +use OCP\ISession; use OCP\IUser; use OCP\IUserManager; use PHPUnit\Framework\MockObject\MockObject; @@ -61,6 +62,9 @@ class ProvisioningServiceTest extends TestCase { /** @var IAvatarManager | MockObject */ private $avatarManager; + /** @var ISession | MockObject */ + private $session; + public function setUp(): void { parent::setUp(); $this->idService = $this->createMock(LocalIdService::class); @@ -75,6 +79,7 @@ public function setUp(): void { $this->clientService = $this->createMock(IClientService::class); $this->avatarManager = $this->createMock(IAvatarManager::class); $this->config = $this->createMock(IConfig::class); + $this->session = $this->createMock(ISession::class); $this->provisioningService = new ProvisioningService( $this->idService, @@ -88,6 +93,7 @@ public function setUp(): void { $this->clientService, $this->avatarManager, $this->config, + $this->session, ); }