Skip to content

Commit 429fa5e

Browse files
authored
Merge pull request #1420 from nextcloud/carl/refactor-userbackend-2
refactor: Harden UserBackend::provisionUser and UserBackend::checkFirstLogin
2 parents f20ed2a + d9b97eb commit 429fa5e

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,
@@ -184,14 +187,23 @@ public function getLogoutUrl(): string {
184187
* Inspired by user_saml
185188
*/
186189
public function getUserData(): array {
187-
$userData = $this->session->get('user_oidc.oidcUserData');
188-
$providerId = (int)$this->session->get(LoginController::PROVIDERID);
189-
$userData = $this->formatUserData($providerId, $userData);
190-
191-
// make sure that a valid UID is given
192-
if (empty($userData['formatted']['uid'])) {
193-
$this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', ['app' => 'user_oidc', 'uid' => $userData['formatted']['uid']]);
194-
throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $userData['formatted']['uid']);
190+
$userData = $this->session->get(self::SESSION_USER_DATA) ?? [];
191+
$rawProviderId = $this->session->get(LoginController::PROVIDERID);
192+
if ($rawProviderId === null) {
193+
throw new \InvalidArgumentException('No OIDC provider ID in session');
194+
}
195+
196+
$providerId = (int)$rawProviderId;
197+
198+
$userData = $this->formatUserData($providerId, is_array($userData) ? $userData : []);
199+
200+
if (!$this->isAcceptableUserId($userData['formatted']['uid'] ?? null)) {
201+
$uid = is_scalar($userData['formatted']['uid'] ?? null) ? (string)$userData['formatted']['uid'] : '';
202+
$this->logger->error('No valid uid given, please check your attribute mapping. Got uid: {uid}', [
203+
'app' => 'user_oidc',
204+
'uid' => $uid,
205+
]);
206+
throw new \InvalidArgumentException('No valid uid given, please check your attribute mapping. Got uid: ' . $uid);
195207
}
196208

197209
return $userData;
@@ -394,6 +406,15 @@ public function getCurrentUserId(): string {
394406
return '';
395407
}
396408

409+
/**
410+
* Returns true only if $userId is a non-empty, non-whitespace-only string.
411+
* Used as a lightweight sanity check on user IDs returned by token validators
412+
* before any database lookup or provisioning takes place.
413+
*/
414+
private function isAcceptableUserId(mixed $userId): bool {
415+
return is_string($userId) && $userId !== '' && trim($userId) !== '';
416+
}
417+
397418
/**
398419
* Set the user in IUserSession after bearer token validation.
399420
* Without this, DI-injected $userId is null in OCS controllers
@@ -420,55 +441,65 @@ private function setSessionUser(string $userId): void {
420441
}
421442

422443
/**
423-
* Inspired by lib/private/User/Session.php::prepareUserLogin()
424444
*
425-
* @param string $userId
426-
* @return bool
427-
* @throws NotFoundException
445+
* Performs first-login initialisation (home folder setup, skeleton copy, events)
446+
* if the user has never logged in before, then updates the last-login timestamp.
447+
* Inspired by lib/private/User/Session.php::prepareUserLogin().
428448
*/
429449
private function checkFirstLogin(string $userId): bool {
430450
$user = $this->userManager->get($userId);
431-
432451
if ($user === null) {
433452
return false;
434453
}
435454

436455
$firstLogin = $user->getLastLogin() === 0;
437456
if ($firstLogin) {
438-
/** @psalm-suppress UndefinedVariable Replace with ServerVersion once we depends on NC 31 */
439-
$OC_Version[0] >= 34 ? Server::get(ISetupManager::class)->setupForUser($user) : \OC_Util::setupFS($userId);
440-
// trigger creation of user home and /files folder
441-
$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
457+
/** @psalm-suppress UndefinedVariable Replace with ServerVersion once we depend on NC 31 */
458+
if ($OC_Version[0] >= 34) {
459+
Server::get(ISetupManager::class)->setupForUser($user);
460+
} else {
461+
\OC_Util::setupFS($userId);
462+
}
463+
442464
try {
465+
// trigger creation of user home and /files folder
466+
$userFolder = Server::get(IRootFolder::class)->getUserFolder($userId);
443467
// copy skeleton
444468
\OC_Util::copySkeleton($userId, $userFolder);
445-
} catch (NotPermittedException $ex) {
446-
// read only uses
469+
} catch (NotFoundException|NotPermittedException $ex) {
470+
$this->logger->warning('Could not set up user folder on first login', ['exception' => $ex]);
447471
}
448472

449-
// trigger any other initialization
450473
$this->eventDispatcher->dispatch(IUser::class . '::firstLogin', new GenericEvent($user));
451474
$this->eventDispatcher->dispatchTyped(new UserFirstTimeLoggedInEvent($user));
452475
}
476+
453477
$user->updateLastLoginTimestamp();
454478
return $firstLogin;
455479
}
456480

457481
/**
458482
* Triggers user provisioning based on the provided strategy
459-
*
460-
* @param string $provisioningStrategyClass
461-
* @param Provider $provider
462-
* @param string $tokenUserId
463-
* @param string $headerToken
464-
* @param IUser|null $existingUser
465-
* @return IUser|null
466483
*/
467484
private function provisionUser(
468-
string $provisioningStrategyClass, Provider $provider, string $tokenUserId, string $headerToken,
485+
string $provisioningStrategyClass,
486+
Provider $provider,
487+
string $tokenUserId,
488+
string $headerToken,
469489
?IUser $existingUser,
470490
): ?IUser {
471-
$provisioningStrategy = Server::get($provisioningStrategyClass);
472-
return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken, $existingUser);
491+
try {
492+
$provisioningStrategy = Server::get($provisioningStrategyClass);
493+
return $provisioningStrategy->provisionUser($provider, $tokenUserId, $headerToken, $existingUser);
494+
} catch (Throwable $e) {
495+
$this->logger->error('Failed to provision user via strategy', [
496+
'strategy' => $provisioningStrategyClass,
497+
'providerId' => $provider->getId(),
498+
'providerIdentifier' => $provider->getIdentifier(),
499+
'userId' => $tokenUserId,
500+
'exception' => $e,
501+
]);
502+
return null;
503+
}
473504
}
474505
}

0 commit comments

Comments
 (0)