Skip to content

Commit eee6d0c

Browse files
CarlSchwansolracsf
andcommitted
refactor: Harden UserBackend::provisionUser and UserBackend::checkFirstLogin
Add more debug logs when something go wrong and more checks for invalid values. Co-Authored-By: Git'Fellow <12234510+solracsf@users.noreply.github.com> Signed-off-by: Carl Schwan <carlschwan@kde.org>
1 parent 8194adf commit eee6d0c

1 file changed

Lines changed: 61 additions & 30 deletions

File tree

lib/User/Backend.php

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
use Throwable;
5050

5151
class Backend extends ABackend implements IPasswordConfirmationBackend, IGetDisplayNameBackend, IApacheBackend, ICustomLogout, ICountUsersBackend {
52+
private const SESSION_USER_DATA = 'user_oidc.oidcUserData';
53+
54+
/** @var list<class-string<IBearerTokenValidator>> */
5255
private $tokenValidators = [
5356
SelfEncodedValidator::class,
5457
UserInfoValidator::class,
@@ -178,14 +181,23 @@ public function getLogoutUrl(): string {
178181
* Inspired by user_saml
179182
*/
180183
public function getUserData(): array {
181-
$userData = $this->session->get('user_oidc.oidcUserData');
182-
$providerId = (int)$this->session->get(LoginController::PROVIDERID);
183-
$userData = $this->formatUserData($providerId, $userData);
184-
185-
// make sure that a valid UID is given
186-
if (empty($userData['formatted']['uid'])) {
187-
$this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', ['app' => 'user_oidc', 'uid' => $userData['formatted']['uid']]);
188-
throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $userData['formatted']['uid']);
184+
$userData = $this->session->get(self::SESSION_USER_DATA) ?? [];
185+
$rawProviderId = $this->session->get(LoginController::PROVIDERID);
186+
if ($rawProviderId === null) {
187+
throw new \InvalidArgumentException('No OIDC provider ID in session');
188+
}
189+
190+
$providerId = (int)$rawProviderId;
191+
192+
$userData = $this->formatUserData($providerId, is_array($userData) ? $userData : []);
193+
194+
if (!$this->isAcceptableUserId($userData['formatted']['uid'] ?? null)) {
195+
$uid = is_scalar($userData['formatted']['uid'] ?? null) ? (string)$userData['formatted']['uid'] : '';
196+
$this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', [
197+
'app' => 'user_oidc',
198+
'uid' => $uid,
199+
]);
200+
throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $uid);
189201
}
190202

191203
return $userData;
@@ -388,6 +400,15 @@ public function getCurrentUserId(): string {
388400
return '';
389401
}
390402

403+
/**
404+
* Returns true only if $userId is a non-empty, non-whitespace-only string.
405+
* Used as a lightweight sanity check on user IDs returned by token validators
406+
* before any database lookup or provisioning takes place.
407+
*/
408+
private function isAcceptableUserId(mixed $userId): bool {
409+
return is_string($userId) && $userId !== '' && trim($userId) !== '';
410+
}
411+
391412
/**
392413
* Set the user in IUserSession after bearer token validation.
393414
* Without this, DI-injected $userId is null in OCS controllers
@@ -414,55 +435,65 @@ private function setSessionUser(string $userId): void {
414435
}
415436

416437
/**
417-
* Inspired by lib/private/User/Session.php::prepareUserLogin()
418438
*
419-
* @param string $userId
420-
* @return bool
421-
* @throws NotFoundException
439+
* Performs first-login initialisation (home folder setup, skeleton copy, events)
440+
* if the user has never logged in before, then updates the last-login timestamp.
441+
* Inspired by lib/private/User/Session.php::prepareUserLogin().
422442
*/
423443
private function checkFirstLogin(string $userId): bool {
424444
$user = $this->userManager->get($userId);
425-
426445
if ($user === null) {
427446
return false;
428447
}
429448

430449
$firstLogin = $user->getLastLogin() === 0;
431450
if ($firstLogin) {
432-
/** @psalm-suppress UndefinedVariable Replace with ServerVersion once we depends on NC 31 */
433-
$OC_Version[0] >= 34 ? Server::get(ISetupManager::class)->setupForUser($user) : \OC_Util::setupFS($userId);
434-
// trigger creation of user home and /files folder
435-
$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
451+
/** @psalm-suppress UndefinedVariable Replace with ServerVersion once we depend on NC 31 */
452+
if ($OC_Version[0] >= 34) {
453+
Server::get(ISetupManager::class)->setupForUser($user);
454+
} else {
455+
\OC_Util::setupFS($userId);
456+
}
457+
436458
try {
459+
// trigger creation of user home and /files folder
460+
$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
437461
// copy skeleton
438462
\OC_Util::copySkeleton($userId, $userFolder);
439-
} catch (NotPermittedException $ex) {
440-
// read only uses
463+
} catch (NotFoundException|NotPermittedException $ex) {
464+
$this->logger->warning('Could not set up user folder on first login', ['exception' => $ex]);
441465
}
442466

443-
// trigger any other initialization
444467
$this->eventDispatcher->dispatch(IUser::class . '::firstLogin', new GenericEvent($user));
445468
$this->eventDispatcher->dispatchTyped(new UserFirstTimeLoggedInEvent($user));
446469
}
470+
447471
$user->updateLastLoginTimestamp();
448472
return $firstLogin;
449473
}
450474

451475
/**
452476
* Triggers user provisioning based on the provided strategy
453-
*
454-
* @param string $provisioningStrategyClass
455-
* @param Provider $provider
456-
* @param string $tokenUserId
457-
* @param string $headerToken
458-
* @param IUser|null $existingUser
459-
* @return IUser|null
460477
*/
461478
private function provisionUser(
462-
string $provisioningStrategyClass, Provider $provider, string $tokenUserId, string $headerToken,
479+
string $provisioningStrategyClass,
480+
Provider $provider,
481+
string $tokenUserId,
482+
string $headerToken,
463483
?IUser $existingUser,
464484
): ?IUser {
465-
$provisioningStrategy = Server::get($provisioningStrategyClass);
466-
return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken, $existingUser);
485+
try {
486+
$provisioningStrategy = Server::get($provisioningStrategyClass);
487+
return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken, $existingUser);
488+
} catch (Throwable $e) {
489+
$this->logger->error('Failed to provision user via strategy', [
490+
'strategy' => $provisioningStrategyClass,
491+
'providerId' => $provider->getId(),
492+
'providerIdentifier' => $provider->getIdentifier(),
493+
'userId' => $tokenUserId,
494+
'exception' => $e,
495+
]);
496+
return null;
497+
}
467498
}
468499
}

0 commit comments

Comments
 (0)