diff options
Diffstat (limited to 'lib/private/Authentication')
22 files changed, 208 insertions, 220 deletions
diff --git a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php index d95bcd98cf9..5781c1edf16 100644 --- a/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php +++ b/lib/private/Authentication/Listeners/RemoteWipeNotificationsListener.php @@ -45,7 +45,7 @@ class RemoteWipeNotificationsListener implements IEventListener { $notification->setApp('auth') ->setUser($token->getUID()) ->setDateTime($this->timeFactory->getDateTime()) - ->setObject('token', (string) $token->getId()) + ->setObject('token', (string)$token->getId()) ->setSubject($event, [ 'name' => $token->getName(), ]); diff --git a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php index 697aea71c6d..a619021d192 100644 --- a/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php +++ b/lib/private/Authentication/Listeners/UserDeletedFilesCleanupListener.php @@ -16,30 +16,34 @@ use OCP\Files\Config\IMountProviderCollection; use OCP\Files\Storage\IStorage; use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\UserDeletedEvent; +use Psr\Log\LoggerInterface; /** @template-implements IEventListener<BeforeUserDeletedEvent|UserDeletedEvent> */ class UserDeletedFilesCleanupListener implements IEventListener { /** @var array<string,IStorage> */ private $homeStorageCache = []; - /** @var IMountProviderCollection */ - private $mountProviderCollection; - - public function __construct(IMountProviderCollection $mountProviderCollection) { - $this->mountProviderCollection = $mountProviderCollection; + public function __construct( + private IMountProviderCollection $mountProviderCollection, + private LoggerInterface $logger, + ) { } public function handle(Event $event): void { + $user = $event->getUser(); + // since we can't reliably get the user home storage after the user is deleted // but the user deletion might get canceled during the before event // we only cache the user home storage during the before event and then do the // action deletion during the after event if ($event instanceof BeforeUserDeletedEvent) { - $userHome = $this->mountProviderCollection->getHomeMountForUser($event->getUser()); + $this->logger->debug('Prepare deleting storage for user {userId}', ['userId' => $user->getUID()]); + + $userHome = $this->mountProviderCollection->getHomeMountForUser($user); $storage = $userHome->getStorage(); if (!$storage) { - throw new \Exception("Account has no home storage"); + throw new \Exception('Account has no home storage'); } // remove all wrappers, so we do the delete directly on the home storage bypassing any wrapper @@ -51,16 +55,18 @@ class UserDeletedFilesCleanupListener implements IEventListener { $this->homeStorageCache[$event->getUser()->getUID()] = $storage; } if ($event instanceof UserDeletedEvent) { - if (!isset($this->homeStorageCache[$event->getUser()->getUID()])) { - throw new \Exception("UserDeletedEvent fired without matching BeforeUserDeletedEvent"); + if (!isset($this->homeStorageCache[$user->getUID()])) { + throw new \Exception('UserDeletedEvent fired without matching BeforeUserDeletedEvent'); } - $storage = $this->homeStorageCache[$event->getUser()->getUID()]; + $storage = $this->homeStorageCache[$user->getUID()]; $cache = $storage->getCache(); $storage->rmdir(''); + $this->logger->debug('Deleted storage for user {userId}', ['userId' => $user->getUID()]); + if ($cache instanceof Cache) { $cache->clear(); } else { - throw new \Exception("Home storage has invalid cache"); + throw new \Exception('Home storage has invalid cache'); } } } diff --git a/lib/private/Authentication/Login/Chain.php b/lib/private/Authentication/Login/Chain.php index abd24287a6c..fc90d9225a7 100644 --- a/lib/private/Authentication/Login/Chain.php +++ b/lib/private/Authentication/Login/Chain.php @@ -9,67 +9,20 @@ declare(strict_types=1); namespace OC\Authentication\Login; class Chain { - /** @var PreLoginHookCommand */ - private $preLoginHookCommand; - - /** @var UserDisabledCheckCommand */ - private $userDisabledCheckCommand; - - /** @var UidLoginCommand */ - private $uidLoginCommand; - - /** @var EmailLoginCommand */ - private $emailLoginCommand; - - /** @var LoggedInCheckCommand */ - private $loggedInCheckCommand; - - /** @var CompleteLoginCommand */ - private $completeLoginCommand; - - /** @var CreateSessionTokenCommand */ - private $createSessionTokenCommand; - - /** @var ClearLostPasswordTokensCommand */ - private $clearLostPasswordTokensCommand; - - /** @var UpdateLastPasswordConfirmCommand */ - private $updateLastPasswordConfirmCommand; - - /** @var SetUserTimezoneCommand */ - private $setUserTimezoneCommand; - - /** @var TwoFactorCommand */ - private $twoFactorCommand; - - /** @var FinishRememberedLoginCommand */ - private $finishRememberedLoginCommand; - - public function __construct(PreLoginHookCommand $preLoginHookCommand, - UserDisabledCheckCommand $userDisabledCheckCommand, - UidLoginCommand $uidLoginCommand, - EmailLoginCommand $emailLoginCommand, - LoggedInCheckCommand $loggedInCheckCommand, - CompleteLoginCommand $completeLoginCommand, - CreateSessionTokenCommand $createSessionTokenCommand, - ClearLostPasswordTokensCommand $clearLostPasswordTokensCommand, - UpdateLastPasswordConfirmCommand $updateLastPasswordConfirmCommand, - SetUserTimezoneCommand $setUserTimezoneCommand, - TwoFactorCommand $twoFactorCommand, - FinishRememberedLoginCommand $finishRememberedLoginCommand + public function __construct( + private PreLoginHookCommand $preLoginHookCommand, + private UserDisabledCheckCommand $userDisabledCheckCommand, + private UidLoginCommand $uidLoginCommand, + private LoggedInCheckCommand $loggedInCheckCommand, + private CompleteLoginCommand $completeLoginCommand, + private CreateSessionTokenCommand $createSessionTokenCommand, + private ClearLostPasswordTokensCommand $clearLostPasswordTokensCommand, + private UpdateLastPasswordConfirmCommand $updateLastPasswordConfirmCommand, + private SetUserTimezoneCommand $setUserTimezoneCommand, + private TwoFactorCommand $twoFactorCommand, + private FinishRememberedLoginCommand $finishRememberedLoginCommand, + private FlowV2EphemeralSessionsCommand $flowV2EphemeralSessionsCommand, ) { - $this->preLoginHookCommand = $preLoginHookCommand; - $this->userDisabledCheckCommand = $userDisabledCheckCommand; - $this->uidLoginCommand = $uidLoginCommand; - $this->emailLoginCommand = $emailLoginCommand; - $this->loggedInCheckCommand = $loggedInCheckCommand; - $this->completeLoginCommand = $completeLoginCommand; - $this->createSessionTokenCommand = $createSessionTokenCommand; - $this->clearLostPasswordTokensCommand = $clearLostPasswordTokensCommand; - $this->updateLastPasswordConfirmCommand = $updateLastPasswordConfirmCommand; - $this->setUserTimezoneCommand = $setUserTimezoneCommand; - $this->twoFactorCommand = $twoFactorCommand; - $this->finishRememberedLoginCommand = $finishRememberedLoginCommand; } public function process(LoginData $loginData): LoginResult { @@ -77,9 +30,9 @@ class Chain { $chain ->setNext($this->userDisabledCheckCommand) ->setNext($this->uidLoginCommand) - ->setNext($this->emailLoginCommand) ->setNext($this->loggedInCheckCommand) ->setNext($this->completeLoginCommand) + ->setNext($this->flowV2EphemeralSessionsCommand) ->setNext($this->createSessionTokenCommand) ->setNext($this->clearLostPasswordTokensCommand) ->setNext($this->updateLastPasswordConfirmCommand) diff --git a/lib/private/Authentication/Login/EmailLoginCommand.php b/lib/private/Authentication/Login/EmailLoginCommand.php deleted file mode 100644 index 96cb39277fd..00000000000 --- a/lib/private/Authentication/Login/EmailLoginCommand.php +++ /dev/null @@ -1,53 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -namespace OC\Authentication\Login; - -use OCP\IUserManager; - -class EmailLoginCommand extends ALoginCommand { - /** @var IUserManager */ - private $userManager; - - public function __construct(IUserManager $userManager) { - $this->userManager = $userManager; - } - - public function process(LoginData $loginData): LoginResult { - if ($loginData->getUser() === false) { - if (!filter_var($loginData->getUsername(), FILTER_VALIDATE_EMAIL)) { - return $this->processNextOrFinishSuccessfully($loginData); - } - - $users = $this->userManager->getByEmail($loginData->getUsername()); - // we only allow login by email if unique - if (count($users) === 1) { - // FIXME: This is a workaround to still stick to configured LDAP login filters - // this can be removed once the email login is properly implemented in the local user backend - // as described in https://github.com/nextcloud/server/issues/5221 - if ($users[0]->getBackendClassName() === 'LDAP') { - return $this->processNextOrFinishSuccessfully($loginData); - } - - $username = $users[0]->getUID(); - if ($username !== $loginData->getUsername()) { - $user = $this->userManager->checkPassword( - $username, - $loginData->getPassword() - ); - if ($user !== false) { - $loginData->setUser($user); - $loginData->setUsername($username); - } - } - } - } - - return $this->processNextOrFinishSuccessfully($loginData); - } -} diff --git a/lib/private/Authentication/Login/FlowV2EphemeralSessionsCommand.php b/lib/private/Authentication/Login/FlowV2EphemeralSessionsCommand.php new file mode 100644 index 00000000000..82dd829334d --- /dev/null +++ b/lib/private/Authentication/Login/FlowV2EphemeralSessionsCommand.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OC\Authentication\Login; + +use OC\Core\Controller\ClientFlowLoginV2Controller; +use OCP\ISession; +use OCP\IURLGenerator; + +class FlowV2EphemeralSessionsCommand extends ALoginCommand { + public function __construct( + private ISession $session, + private IURLGenerator $urlGenerator, + ) { + } + + public function process(LoginData $loginData): LoginResult { + $loginV2GrantRoute = $this->urlGenerator->linkToRoute('core.ClientFlowLoginV2.grantPage'); + if (str_starts_with($loginData->getRedirectUrl() ?? '', $loginV2GrantRoute)) { + $this->session->set(ClientFlowLoginV2Controller::EPHEMERAL_NAME, true); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } +} diff --git a/lib/private/Authentication/Login/WebAuthnChain.php b/lib/private/Authentication/Login/WebAuthnChain.php index c31e39de28c..ae523c43da6 100644 --- a/lib/private/Authentication/Login/WebAuthnChain.php +++ b/lib/private/Authentication/Login/WebAuthnChain.php @@ -48,7 +48,7 @@ class WebAuthnChain { UpdateLastPasswordConfirmCommand $updateLastPasswordConfirmCommand, SetUserTimezoneCommand $setUserTimezoneCommand, TwoFactorCommand $twoFactorCommand, - FinishRememberedLoginCommand $finishRememberedLoginCommand + FinishRememberedLoginCommand $finishRememberedLoginCommand, ) { $this->userDisabledCheckCommand = $userDisabledCheckCommand; $this->webAuthnLoginCommand = $webAuthnLoginCommand; diff --git a/lib/private/Authentication/LoginCredentials/Credentials.php b/lib/private/Authentication/LoginCredentials/Credentials.php index 2d7ed3adfd0..3414034b33c 100644 --- a/lib/private/Authentication/LoginCredentials/Credentials.php +++ b/lib/private/Authentication/LoginCredentials/Credentials.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/lib/private/Authentication/LoginCredentials/Store.php b/lib/private/Authentication/LoginCredentials/Store.php index bd39dd11460..67c5712715c 100644 --- a/lib/private/Authentication/LoginCredentials/Store.php +++ b/lib/private/Authentication/LoginCredentials/Store.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OC\Authentication\LoginCredentials; +use Exception; use OC\Authentication\Exceptions\PasswordlessTokenException; use OC\Authentication\Token\IProvider; use OCP\Authentication\Exceptions\CredentialsUnavailableException; @@ -15,6 +16,7 @@ use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\LoginCredentials\ICredentials; use OCP\Authentication\LoginCredentials\IStore; use OCP\ISession; +use OCP\Security\ICrypto; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\Util; use Psr\Log\LoggerInterface; @@ -29,9 +31,12 @@ class Store implements IStore { /** @var IProvider|null */ private $tokenProvider; - public function __construct(ISession $session, + public function __construct( + ISession $session, LoggerInterface $logger, - ?IProvider $tokenProvider = null) { + private readonly ICrypto $crypto, + ?IProvider $tokenProvider = null, + ) { $this->session = $session; $this->logger = $logger; $this->tokenProvider = $tokenProvider; @@ -45,6 +50,9 @@ class Store implements IStore { * @param array $params */ public function authenticate(array $params) { + if ($params['password'] !== null) { + $params['password'] = $this->crypto->encrypt((string)$params['password']); + } $this->session->set('login_credentials', json_encode($params)); } @@ -91,6 +99,13 @@ class Store implements IStore { if ($trySession && $this->session->exists('login_credentials')) { /** @var array $creds */ $creds = json_decode($this->session->get('login_credentials'), true); + if ($creds['password'] !== null) { + try { + $creds['password'] = $this->crypto->decrypt($creds['password']); + } catch (Exception $e) { + //decryption failed, continue with old password as it is + } + } return new Credentials( $creds['uid'], $creds['loginName'] ?? $this->session->get('loginname') ?? $creds['uid'], // Pre 20 didn't have a loginName property, hence fall back to the session value and then to the UID diff --git a/lib/private/Authentication/Notifications/Notifier.php b/lib/private/Authentication/Notifications/Notifier.php index 3b6c9b3e610..a81e385d8b1 100644 --- a/lib/private/Authentication/Notifications/Notifier.php +++ b/lib/private/Authentication/Notifications/Notifier.php @@ -8,10 +8,10 @@ declare(strict_types=1); */ namespace OC\Authentication\Notifications; -use InvalidArgumentException; use OCP\L10N\IFactory as IL10nFactory; use OCP\Notification\INotification; use OCP\Notification\INotifier; +use OCP\Notification\UnknownNotificationException; class Notifier implements INotifier { /** @var IL10nFactory */ @@ -27,7 +27,7 @@ class Notifier implements INotifier { public function prepare(INotification $notification, string $languageCode): INotification { if ($notification->getApp() !== 'auth') { // Not my app => throw - throw new InvalidArgumentException(); + throw new UnknownNotificationException(); } // Read the language from the notification @@ -52,7 +52,7 @@ class Notifier implements INotifier { return $notification; default: // Unknown subject => Unknown notification => throw - throw new InvalidArgumentException(); + throw new UnknownNotificationException(); } } diff --git a/lib/private/Authentication/Token/IProvider.php b/lib/private/Authentication/Token/IProvider.php index dfb17301ab3..d47427e79bf 100644 --- a/lib/private/Authentication/Token/IProvider.php +++ b/lib/private/Authentication/Token/IProvider.php @@ -35,7 +35,9 @@ interface IProvider { ?string $password, string $name, int $type = OCPIToken::TEMPORARY_TOKEN, - int $remember = OCPIToken::DO_NOT_REMEMBER): OCPIToken; + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken; /** * Get a token by token id diff --git a/lib/private/Authentication/Token/Manager.php b/lib/private/Authentication/Token/Manager.php index 37ed6083d82..6953f47b004 100644 --- a/lib/private/Authentication/Token/Manager.php +++ b/lib/private/Authentication/Token/Manager.php @@ -42,7 +42,9 @@ class Manager implements IProvider, OCPIProvider { $password, string $name, int $type = OCPIToken::TEMPORARY_TOKEN, - int $remember = OCPIToken::DO_NOT_REMEMBER): OCPIToken { + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken { if (mb_strlen($name) > 128) { $name = mb_substr($name, 0, 120) . '…'; } @@ -55,7 +57,8 @@ class Manager implements IProvider, OCPIProvider { $password, $name, $type, - $remember + $remember, + $scope, ); } catch (UniqueConstraintViolationException $e) { // It's rare, but if two requests of the same session (e.g. env-based SAML) diff --git a/lib/private/Authentication/Token/PublicKeyToken.php b/lib/private/Authentication/Token/PublicKeyToken.php index 961b7191d84..be427ab4839 100644 --- a/lib/private/Authentication/Token/PublicKeyToken.php +++ b/lib/private/Authentication/Token/PublicKeyToken.php @@ -10,6 +10,7 @@ namespace OC\Authentication\Token; use OCP\AppFramework\Db\Entity; use OCP\Authentication\Token\IToken; +use OCP\DB\Types; /** * @method void setId(int $id) @@ -88,16 +89,16 @@ class PublicKeyToken extends Entity implements INamedToken, IWipeableToken { $this->addType('passwordHash', 'string'); $this->addType('name', 'string'); $this->addType('token', 'string'); - $this->addType('type', 'int'); - $this->addType('remember', 'int'); - $this->addType('lastActivity', 'int'); - $this->addType('lastCheck', 'int'); + $this->addType('type', Types::INTEGER); + $this->addType('remember', Types::INTEGER); + $this->addType('lastActivity', Types::INTEGER); + $this->addType('lastCheck', Types::INTEGER); $this->addType('scope', 'string'); - $this->addType('expires', 'int'); + $this->addType('expires', Types::INTEGER); $this->addType('publicKey', 'string'); $this->addType('privateKey', 'string'); - $this->addType('version', 'int'); - $this->addType('passwordInvalid', 'bool'); + $this->addType('version', Types::INTEGER); + $this->addType('passwordInvalid', Types::BOOLEAN); } public function getId(): int { diff --git a/lib/private/Authentication/Token/PublicKeyTokenMapper.php b/lib/private/Authentication/Token/PublicKeyTokenMapper.php index 0db5c4f53e7..9aabd69e57a 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenMapper.php +++ b/lib/private/Authentication/Token/PublicKeyTokenMapper.php @@ -31,22 +31,25 @@ class PublicKeyTokenMapper extends QBMapper { $qb->delete($this->tableName) ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeStatement(); } /** * @param int $olderThan - * @param int $remember + * @param int $type + * @param int|null $remember */ - public function invalidateOld(int $olderThan, int $remember = IToken::DO_NOT_REMEMBER) { + public function invalidateOld(int $olderThan, int $type = IToken::TEMPORARY_TOKEN, ?int $remember = null) { /* @var $qb IQueryBuilder */ $qb = $this->db->getQueryBuilder(); - $qb->delete($this->tableName) + $delete = $qb->delete($this->tableName) ->where($qb->expr()->lt('last_activity', $qb->createNamedParameter($olderThan, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter(IToken::TEMPORARY_TOKEN, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))) - ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->andWhere($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT))) + ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); + if ($remember !== null) { + $delete->andWhere($qb->expr()->eq('remember', $qb->createNamedParameter($remember, IQueryBuilder::PARAM_INT))); + } + $delete->executeStatement(); } public function invalidateLastUsedBefore(string $uid, int $before): int { @@ -70,7 +73,7 @@ class PublicKeyTokenMapper extends QBMapper { ->from($this->tableName) ->where($qb->expr()->eq('token', $qb->createNamedParameter($token))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeQuery(); $data = $result->fetch(); $result->closeCursor(); @@ -92,7 +95,7 @@ class PublicKeyTokenMapper extends QBMapper { ->from($this->tableName) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) - ->execute(); + ->executeQuery(); $data = $result->fetch(); $result->closeCursor(); @@ -119,7 +122,7 @@ class PublicKeyTokenMapper extends QBMapper { ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))) ->setMaxResults(1000); - $result = $qb->execute(); + $result = $qb->executeQuery(); $data = $result->fetchAll(); $result->closeCursor(); @@ -151,7 +154,7 @@ class PublicKeyTokenMapper extends QBMapper { $qb->delete($this->tableName) ->where($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR)) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); - $qb->execute(); + $qb->executeStatement(); } public function deleteTempToken(PublicKeyToken $except) { @@ -163,7 +166,7 @@ class PublicKeyTokenMapper extends QBMapper { ->andWhere($qb->expr()->neq('id', $qb->createNamedParameter($except->getId()))) ->andWhere($qb->expr()->eq('version', $qb->createNamedParameter(PublicKeyToken::VERSION, IQueryBuilder::PARAM_INT))); - $qb->execute(); + $qb->executeStatement(); } public function hasExpiredTokens(string $uid): bool { @@ -174,7 +177,7 @@ class PublicKeyTokenMapper extends QBMapper { ->andWhere($qb->expr()->eq('password_invalid', $qb->createNamedParameter(true), IQueryBuilder::PARAM_BOOL)) ->setMaxResults(1); - $cursor = $qb->execute(); + $cursor = $qb->executeQuery(); $data = $cursor->fetchAll(); $cursor->closeCursor(); diff --git a/lib/private/Authentication/Token/PublicKeyTokenProvider.php b/lib/private/Authentication/Token/PublicKeyTokenProvider.php index 767ece1e551..12c3a1d535b 100644 --- a/lib/private/Authentication/Token/PublicKeyTokenProvider.php +++ b/lib/private/Authentication/Token/PublicKeyTokenProvider.php @@ -85,7 +85,9 @@ class PublicKeyTokenProvider implements IProvider { ?string $password, string $name, int $type = OCPIToken::TEMPORARY_TOKEN, - int $remember = OCPIToken::DO_NOT_REMEMBER): OCPIToken { + int $remember = OCPIToken::DO_NOT_REMEMBER, + ?array $scope = null, + ): OCPIToken { if (strlen($token) < self::TOKEN_MIN_LENGTH) { $exception = new InvalidTokenException('Token is too short, minimum of ' . self::TOKEN_MIN_LENGTH . ' characters is required, ' . strlen($token) . ' characters given'); $this->logger->error('Invalid token provided when generating new token', ['exception' => $exception]); @@ -107,6 +109,10 @@ class PublicKeyTokenProvider implements IProvider { $dbToken->setPasswordHash($randomOldToken->getPasswordHash()); } + if ($scope !== null) { + $dbToken->setScope($scope); + } + $this->mapper->insert($dbToken); if (!$oldTokenMatches && $password !== null) { @@ -156,7 +162,7 @@ class PublicKeyTokenProvider implements IProvider { $this->rotate($token, $tokenId, $tokenId); } catch (DoesNotExistException) { $this->cacheInvalidHash($tokenHash); - throw new InvalidTokenException("Token does not exist: " . $ex->getMessage(), 0, $ex); + throw new InvalidTokenException('Token does not exist: ' . $ex->getMessage(), 0, $ex); } } @@ -171,7 +177,7 @@ class PublicKeyTokenProvider implements IProvider { private function getTokenFromCache(string $tokenHash): ?PublicKeyToken { $serializedToken = $this->cache->get($tokenHash); if ($serializedToken === false) { - throw new InvalidTokenException('Token does not exist: ' . $tokenHash); + return null; } if ($serializedToken === null) { @@ -226,7 +232,7 @@ class PublicKeyTokenProvider implements IProvider { $token = $this->getToken($oldSessionId); if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $password = null; @@ -234,6 +240,8 @@ class PublicKeyTokenProvider implements IProvider { $privateKey = $this->decrypt($token->getPrivateKey(), $oldSessionId); $password = $this->decryptPassword($token->getPassword(), $privateKey); } + + $scope = $token->getScope() === '' ? null : $token->getScopeAsArray(); $newToken = $this->generateToken( $sessionId, $token->getUID(), @@ -241,9 +249,9 @@ class PublicKeyTokenProvider implements IProvider { $password, $token->getName(), OCPIToken::TEMPORARY_TOKEN, - $token->getRemember() + $token->getRemember(), + $scope, ); - $newToken->setScope($token->getScopeAsArray()); $this->cacheToken($newToken); $this->cacheInvalidHash($token->getToken()); @@ -273,10 +281,19 @@ class PublicKeyTokenProvider implements IProvider { public function invalidateOldTokens() { $olderThan = $this->time->getTime() - $this->config->getSystemValueInt('session_lifetime', 60 * 60 * 24); $this->logger->debug('Invalidating session tokens older than ' . date('c', $olderThan), ['app' => 'cron']); - $this->mapper->invalidateOld($olderThan, OCPIToken::DO_NOT_REMEMBER); + $this->mapper->invalidateOld($olderThan, OCPIToken::TEMPORARY_TOKEN, OCPIToken::DO_NOT_REMEMBER); + $rememberThreshold = $this->time->getTime() - $this->config->getSystemValueInt('remember_login_cookie_lifetime', 60 * 60 * 24 * 15); $this->logger->debug('Invalidating remembered session tokens older than ' . date('c', $rememberThreshold), ['app' => 'cron']); - $this->mapper->invalidateOld($rememberThreshold, OCPIToken::REMEMBER); + $this->mapper->invalidateOld($rememberThreshold, OCPIToken::TEMPORARY_TOKEN, OCPIToken::REMEMBER); + + $wipeThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_wipe_token_retention', 60 * 60 * 24 * 60); + $this->logger->debug('Invalidating auth tokens marked for remote wipe older than ' . date('c', $wipeThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($wipeThreshold, OCPIToken::WIPE_TOKEN); + + $authTokenThreshold = $this->time->getTime() - $this->config->getSystemValueInt('token_auth_token_retention', 60 * 60 * 24 * 365); + $this->logger->debug('Invalidating auth tokens older than ' . date('c', $authTokenThreshold), ['app' => 'cron']); + $this->mapper->invalidateOld($authTokenThreshold, OCPIToken::PERMANENT_TOKEN); } public function invalidateLastUsedBefore(string $uid, int $before): void { @@ -285,7 +302,7 @@ class PublicKeyTokenProvider implements IProvider { public function updateToken(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $this->mapper->update($token); $this->cacheToken($token); @@ -293,7 +310,7 @@ class PublicKeyTokenProvider implements IProvider { public function updateTokenActivity(OCPIToken $token) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $activityInterval = $this->config->getSystemValueInt('token_auth_activity_update', 60); @@ -314,7 +331,7 @@ class PublicKeyTokenProvider implements IProvider { public function getPassword(OCPIToken $savedToken, string $tokenId): string { if (!($savedToken instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } if ($savedToken->getPassword() === null) { @@ -330,7 +347,7 @@ class PublicKeyTokenProvider implements IProvider { public function setPassword(OCPIToken $token, string $tokenId, string $password) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $this->atomic(function () use ($password, $token) { @@ -355,7 +372,7 @@ class PublicKeyTokenProvider implements IProvider { public function rotate(OCPIToken $token, string $oldTokenId, string $newTokenId): OCPIToken { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } // Decrypt private key with oldTokenId @@ -388,7 +405,7 @@ class PublicKeyTokenProvider implements IProvider { } catch (\Exception $ex2) { // Delete the invalid token $this->invalidateToken($token); - throw new InvalidTokenException("Could not decrypt token password: " . $ex->getMessage(), 0, $ex2); + throw new InvalidTokenException('Could not decrypt token password: ' . $ex->getMessage(), 0, $ex2); } } } @@ -413,7 +430,7 @@ class PublicKeyTokenProvider implements IProvider { } /** - * @deprecated Fallback for instances where the secret might not have been set by accident + * @deprecated 26.0.0 Fallback for instances where the secret might not have been set by accident */ private function hashTokenWithEmptySecret(string $token): string { return hash('sha512', $token); @@ -478,7 +495,7 @@ class PublicKeyTokenProvider implements IProvider { public function markPasswordInvalid(OCPIToken $token, string $tokenId) { if (!($token instanceof PublicKeyToken)) { - throw new InvalidTokenException("Invalid token type"); + throw new InvalidTokenException('Invalid token type'); } $token->setPasswordInvalid(true); diff --git a/lib/private/Authentication/Token/RemoteWipe.php b/lib/private/Authentication/Token/RemoteWipe.php index 43c2bd060d1..80ba330b66d 100644 --- a/lib/private/Authentication/Token/RemoteWipe.php +++ b/lib/private/Authentication/Token/RemoteWipe.php @@ -98,7 +98,7 @@ class RemoteWipe { $dbToken = $e->getToken(); - $this->logger->info("user " . $dbToken->getUID() . " started a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' started a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeStarted::class, new RemoteWipeStarted($dbToken)); @@ -126,7 +126,7 @@ class RemoteWipe { $this->tokenProvider->invalidateToken($token); - $this->logger->info("user " . $dbToken->getUID() . " finished a remote wipe"); + $this->logger->info('user ' . $dbToken->getUID() . ' finished a remote wipe'); $this->eventDispatcher->dispatch(RemoteWipeFinished::class, new RemoteWipeFinished($dbToken)); return true; diff --git a/lib/private/Authentication/Token/TokenCleanupJob.php b/lib/private/Authentication/Token/TokenCleanupJob.php index 041d2e8a5e2..e6d1e69e9b4 100644 --- a/lib/private/Authentication/Token/TokenCleanupJob.php +++ b/lib/private/Authentication/Token/TokenCleanupJob.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-only diff --git a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php index c84b7f1af20..cc468dbeba0 100644 --- a/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php +++ b/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php @@ -29,7 +29,7 @@ class ProviderUserAssignmentDao { * Get all assigned provider IDs for the given user ID * * @return array<string, bool> where the array key is the provider ID (string) and the - * value is the enabled state (bool) + * value is the enabled state (bool) */ public function getState(string $uid): array { $qb = $this->conn->getQueryBuilder(); @@ -37,7 +37,7 @@ class ProviderUserAssignmentDao { $query = $qb->select('provider_id', 'enabled') ->from(self::TABLE_NAME) ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); - $result = $query->execute(); + $result = $query->executeQuery(); $providers = []; foreach ($result->fetchAll() as $row) { $providers[(string)$row['provider_id']] = (int)$row['enabled'] === 1; @@ -95,7 +95,7 @@ class ProviderUserAssignmentDao { return [ 'provider_id' => (string)$row['provider_id'], 'uid' => (string)$row['uid'], - 'enabled' => ((int) $row['enabled']) === 1, + 'enabled' => ((int)$row['enabled']) === 1, ]; }, $rows)); } @@ -106,6 +106,6 @@ class ProviderUserAssignmentDao { $deleteQuery = $qb->delete(self::TABLE_NAME) ->where($qb->expr()->eq('provider_id', $qb->createNamedParameter($providerId))); - $deleteQuery->execute(); + $deleteQuery->executeStatement(); } } diff --git a/lib/private/Authentication/TwoFactorAuth/Manager.php b/lib/private/Authentication/TwoFactorAuth/Manager.php index 2585646c998..07aa98610ed 100644 --- a/lib/private/Authentication/TwoFactorAuth/Manager.php +++ b/lib/private/Authentication/TwoFactorAuth/Manager.php @@ -12,6 +12,7 @@ use BadMethodCallException; use Exception; use OC\Authentication\Token\IProvider as TokenProvider; use OCP\Activity\IManager; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\TwoFactorAuth\IActivatableAtLogin; @@ -192,7 +193,7 @@ class Manager { if (!empty($missing)) { // There was at least one provider missing - $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']); + $this->logger->alert(count($missing) . ' two-factor auth providers failed to load', ['app' => 'core']); return true; } @@ -307,8 +308,8 @@ class Manager { // First check if the session tells us we should do 2FA (99% case) if (!$this->session->exists(self::SESSION_UID_KEY)) { // Check if the session tells us it is 2FA authenticated already - if ($this->session->exists(self::SESSION_UID_DONE) && - $this->session->get(self::SESSION_UID_DONE) === $user->getUID()) { + if ($this->session->exists(self::SESSION_UID_DONE) + && $this->session->get(self::SESSION_UID_DONE) === $user->getUID()) { return false; } @@ -322,7 +323,7 @@ class Manager { $tokenId = $token->getId(); $tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa'); - if (!\in_array((string) $tokenId, $tokensNeeding2FA, true)) { + if (!\in_array((string)$tokenId, $tokensNeeding2FA, true)) { $this->session->set(self::SESSION_UID_DONE, $user->getUID()); return false; } @@ -359,14 +360,19 @@ class Manager { $id = $this->session->getId(); $token = $this->tokenProvider->getToken($id); - $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string) $token->getId(), (string)$this->timeFactory->getTime()); + $this->config->setUserValue($user->getUID(), 'login_token_2fa', (string)$token->getId(), (string)$this->timeFactory->getTime()); } public function clearTwoFactorPending(string $userId) { $tokensNeeding2FA = $this->config->getUserKeys($userId, 'login_token_2fa'); foreach ($tokensNeeding2FA as $tokenId) { - $this->tokenProvider->invalidateTokenById($userId, (int)$tokenId); + $this->config->deleteUserValue($userId, 'login_token_2fa', $tokenId); + + try { + $this->tokenProvider->invalidateTokenById($userId, (int)$tokenId); + } catch (DoesNotExistException $e) { + } } } } diff --git a/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php index b9a0a97bec4..7e674a01dd8 100644 --- a/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php +++ b/lib/private/Authentication/TwoFactorAuth/ProviderLoader.php @@ -9,8 +9,7 @@ declare(strict_types=1); namespace OC\Authentication\TwoFactorAuth; use Exception; -use OC; -use OC_App; +use OC\AppFramework\Bootstrap\Coordinator; use OCP\App\IAppManager; use OCP\AppFramework\QueryException; use OCP\Authentication\TwoFactorAuth\IProvider; @@ -19,15 +18,10 @@ use OCP\IUser; class ProviderLoader { public const BACKUP_CODES_APP_ID = 'twofactor_backupcodes'; - /** @var IAppManager */ - private $appManager; - - /** @var OC\AppFramework\Bootstrap\Coordinator */ - private $coordinator; - - public function __construct(IAppManager $appManager, OC\AppFramework\Bootstrap\Coordinator $coordinator) { - $this->appManager = $appManager; - $this->coordinator = $coordinator; + public function __construct( + private IAppManager $appManager, + private Coordinator $coordinator, + ) { } /** @@ -58,12 +52,12 @@ class ProviderLoader { } } - $registeredProviders = $this->coordinator->getRegistrationContext()->getTwoFactorProviders(); + $registeredProviders = $this->coordinator->getRegistrationContext()?->getTwoFactorProviders() ?? []; foreach ($registeredProviders as $provider) { try { $this->loadTwoFactorApp($provider->getAppId()); - $provider = \OCP\Server::get($provider->getService()); - $providers[$provider->getId()] = $provider; + $providerInstance = \OCP\Server::get($provider->getService()); + $providers[$providerInstance->getId()] = $providerInstance; } catch (QueryException $exc) { // Provider class can not be resolved throw new Exception('Could not load two-factor auth provider ' . $provider->getService()); @@ -75,12 +69,10 @@ class ProviderLoader { /** * Load an app by ID if it has not been loaded yet - * - * @param string $appId */ - protected function loadTwoFactorApp(string $appId) { - if (!OC_App::isAppLoaded($appId)) { - OC_App::loadApp($appId); + protected function loadTwoFactorApp(string $appId): void { + if (!$this->appManager->isAppLoaded($appId)) { + $this->appManager->loadApp($appId); } } } diff --git a/lib/private/Authentication/WebAuthn/CredentialRepository.php b/lib/private/Authentication/WebAuthn/CredentialRepository.php index f32136f9594..203f2ef9020 100644 --- a/lib/private/Authentication/WebAuthn/CredentialRepository.php +++ b/lib/private/Authentication/WebAuthn/CredentialRepository.php @@ -44,7 +44,7 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository { }, $entities); } - public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null): PublicKeyCredentialEntity { + public function saveAndReturnCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource, ?string $name = null, bool $userVerification = false): PublicKeyCredentialEntity { $oldEntity = null; try { @@ -58,13 +58,18 @@ class CredentialRepository implements PublicKeyCredentialSourceRepository { $name = 'default'; } - $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource); + $entity = PublicKeyCredentialEntity::fromPublicKeyCrendentialSource($name, $publicKeyCredentialSource, $userVerification); if ($oldEntity) { $entity->setId($oldEntity->getId()); if ($defaultName) { $entity->setName($oldEntity->getName()); } + + // Don't downgrade UV just because it was skipped during a login due to another key + if ($oldEntity->getUserVerification()) { + $entity->setUserVerification(true); + } } return $this->credentialMapper->insertOrUpdate($entity); diff --git a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php index 443a7985cae..6c4bc3ca81b 100644 --- a/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php +++ b/lib/private/Authentication/WebAuthn/Db/PublicKeyCredentialEntity.php @@ -23,6 +23,10 @@ use Webauthn\PublicKeyCredentialSource; * @method void setPublicKeyCredentialId(string $id); * @method string getData(); * @method void setData(string $data); + * + * @since 30.0.0 Add userVerification attribute + * @method bool|null getUserVerification(); + * @method void setUserVerification(bool $userVerification); */ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ @@ -37,20 +41,25 @@ class PublicKeyCredentialEntity extends Entity implements JsonSerializable { /** @var string */ protected $data; + /** @var bool|null */ + protected $userVerification; + public function __construct() { $this->addType('name', 'string'); $this->addType('uid', 'string'); $this->addType('publicKeyCredentialId', 'string'); $this->addType('data', 'string'); + $this->addType('userVerification', 'boolean'); } - public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource): PublicKeyCredentialEntity { + public static function fromPublicKeyCrendentialSource(string $name, PublicKeyCredentialSource $publicKeyCredentialSource, bool $userVerification): PublicKeyCredentialEntity { $publicKeyCredentialEntity = new self(); $publicKeyCredentialEntity->setName($name); $publicKeyCredentialEntity->setUid($publicKeyCredentialSource->getUserHandle()); $publicKeyCredentialEntity->setPublicKeyCredentialId(base64_encode($publicKeyCredentialSource->getPublicKeyCredentialId())); $publicKeyCredentialEntity->setData(json_encode($publicKeyCredentialSource)); + $publicKeyCredentialEntity->setUserVerification($userVerification); return $publicKeyCredentialEntity; } diff --git a/lib/private/Authentication/WebAuthn/Manager.php b/lib/private/Authentication/WebAuthn/Manager.php index 007be245992..96dc0719b54 100644 --- a/lib/private/Authentication/WebAuthn/Manager.php +++ b/lib/private/Authentication/WebAuthn/Manager.php @@ -53,7 +53,7 @@ class Manager { CredentialRepository $repository, PublicKeyCredentialMapper $credentialMapper, LoggerInterface $logger, - IConfig $config + IConfig $config, ) { $this->repository = $repository; $this->credentialMapper = $credentialMapper; @@ -88,8 +88,8 @@ class Manager { ]; $authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria( - null, - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + AuthenticatorSelectionCriteria::AUTHENTICATOR_ATTACHMENT_NO_PREFERENCE, + AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_PREFERRED, null, false, ); @@ -151,7 +151,8 @@ class Manager { } // Persist the data - return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name); + $userVerification = $response->attestationObject->authData->isUserVerified(); + return $this->repository->saveAndReturnCredentialSource($publicKeyCredentialSource, $name, $userVerification); } private function stripPort(string $serverHost): string { @@ -160,7 +161,11 @@ class Manager { public function startAuthentication(string $uid, string $serverHost): PublicKeyCredentialRequestOptions { // List of registered PublicKeyCredentialDescriptor classes associated to the user - $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED; + $registeredPublicKeyCredentialDescriptors = array_map(function (PublicKeyCredentialEntity $entity) use (&$userVerificationRequirement) { + if ($entity->getUserVerification() !== true) { + $userVerificationRequirement = AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED; + } $credential = $entity->toPublicKeyCredentialSource(); return new PublicKeyCredentialDescriptor( $credential->type, @@ -173,7 +178,7 @@ class Manager { random_bytes(32), // Challenge $this->stripPort($serverHost), // Relying Party ID $registeredPublicKeyCredentialDescriptors, // Registered PublicKeyCredentialDescriptor classes - AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + $userVerificationRequirement, 60000, // Timeout ); } @@ -241,14 +246,6 @@ class Manager { } public function isWebAuthnAvailable(): bool { - if (!extension_loaded('bcmath')) { - return false; - } - - if (!extension_loaded('gmp')) { - return false; - } - if (!$this->config->getSystemValueBool('auth.webauthn.enabled', true)) { return false; } |