diff options
author | Louis Chemineau <louis@chmn.me> | 2025-02-11 11:28:31 +0100 |
---|---|---|
committer | Louis Chemineau <louis@chmn.me> | 2025-02-11 11:56:30 +0100 |
commit | 8a8a92b88f9d3c0452abf1bc5455265d49c0765a (patch) | |
tree | 6c0818ba2aa546443b6cc2a2cd4032e6022b1b14 /lib/private | |
parent | c64bb7c3cdc745d490478f1fea93e6de8be02ab2 (diff) | |
download | nextcloud-server-8a8a92b88f9d3c0452abf1bc5455265d49c0765a.tar.gz nextcloud-server-8a8a92b88f9d3c0452abf1bc5455265d49c0765a.zip |
feat: Use inline password confirmation in external storage settings
Signed-off-by: Louis Chemineau <louis@chmn.me>
Diffstat (limited to 'lib/private')
-rw-r--r-- | lib/private/AppFramework/DependencyInjection/DIContainer.php | 4 | ||||
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php | 125 |
2 files changed, 64 insertions, 65 deletions
diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index f920ff4f207..7583a69e0d6 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -50,6 +50,7 @@ use OC\Diagnostics\EventLogger; use OC\Log\PsrLoggerAdapter; use OC\ServerContainer; use OC\Settings\AuthorizedGroupMapper; +use OC\User\Manager as UserManager; use OCA\WorkflowEngine\Manager; use OCP\AppFramework\Http\IOutput; use OCP\AppFramework\IAppContainer; @@ -278,6 +279,9 @@ class DIContainer extends SimpleContainer implements IAppContainer { $c->get(IUserSession::class), $c->get(ITimeFactory::class), $c->get(\OC\Authentication\Token\IProvider::class), + $c->get(LoggerInterface::class), + $c->get(IRequest::class), + $c->get(UserManager::class), ) ); $dispatcher->registerMiddleware( diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index 8d00f6b7423..b967124883c 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -26,6 +26,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\NotConfirmedException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Authentication\Token\IProvider; +use OC\User\Manager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Middleware; @@ -33,81 +34,76 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Authentication\Exceptions\ExpiredTokenException; use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; use OCP\Session\Exceptions\SessionNotAvailableException; use OCP\User\Backend\IPasswordConfirmationBackend; +use Psr\Log\LoggerInterface; use ReflectionMethod; class PasswordConfirmationMiddleware extends Middleware { - /** @var ControllerMethodReflector */ - private $reflector; - /** @var ISession */ - private $session; - /** @var IUserSession */ - private $userSession; - /** @var ITimeFactory */ - private $timeFactory; - /** @var array */ - private $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - private IProvider $tokenProvider; + private array $excludedUserBackEnds = ['user_saml' => true, 'user_globalsiteselector' => true]; - /** - * PasswordConfirmationMiddleware constructor. - * - * @param ControllerMethodReflector $reflector - * @param ISession $session - * @param IUserSession $userSession - * @param ITimeFactory $timeFactory - */ - public function __construct(ControllerMethodReflector $reflector, - ISession $session, - IUserSession $userSession, - ITimeFactory $timeFactory, - IProvider $tokenProvider, + public function __construct( + private ControllerMethodReflector $reflector, + private ISession $session, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private IProvider $tokenProvider, + private LoggerInterface $logger, + private IRequest $request, + private Manager $userManager, ) { - $this->reflector = $reflector; - $this->session = $session; - $this->userSession = $userSession; - $this->timeFactory = $timeFactory; - $this->tokenProvider = $tokenProvider; } /** - * @param Controller $controller - * @param string $methodName * @throws NotConfirmedException */ - public function beforeController($controller, $methodName) { + public function beforeController(Controller $controller, string $methodName) { $reflectionMethod = new ReflectionMethod($controller, $methodName); - if ($this->hasAnnotationOrAttribute($reflectionMethod, 'PasswordConfirmationRequired', PasswordConfirmationRequired::class)) { - $user = $this->userSession->getUser(); - $backendClassName = ''; - if ($user !== null) { - $backend = $user->getBackend(); - if ($backend instanceof IPasswordConfirmationBackend) { - if (!$backend->canConfirmPassword($user->getUID())) { - return; - } - } + if (!$this->needsPasswordConfirmation($reflectionMethod)) { + return; + } - $backendClassName = $user->getBackendClassName(); + $user = $this->userSession->getUser(); + $backendClassName = ''; + if ($user !== null) { + $backend = $user->getBackend(); + if ($backend instanceof IPasswordConfirmationBackend) { + if (!$backend->canConfirmPassword($user->getUID())) { + return; + } } - try { - $sessionId = $this->session->getId(); - $token = $this->tokenProvider->getToken($sessionId); - } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { - // States we do not deal with here. - return; - } - $scope = $token->getScopeAsArray(); - if (isset($scope['password-unconfirmable']) && $scope['password-unconfirmable'] === true) { - // Users logging in from SSO backends cannot confirm their password by design - return; + $backendClassName = $user->getBackendClassName(); + } + + try { + $sessionId = $this->session->getId(); + $token = $this->tokenProvider->getToken($sessionId); + } catch (SessionNotAvailableException|InvalidTokenException|WipeTokenException|ExpiredTokenException) { + // States we do not deal with here. + return; + } + + $scope = $token->getScopeAsArray(); + if (isset($scope['password-unconfirmable']) && $scope['password-unconfirmable'] === true) { + // Users logging in from SSO backends cannot confirm their password by design + return; + } + + if ($this->isPasswordConfirmationStrict($reflectionMethod)) { + $authHeader = $this->request->getHeader('Authorization'); + [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2); + $loginResult = $this->userManager->checkPassword($user->getUid(), $password); + if ($loginResult === false) { + throw new NotConfirmedException(); } + $this->session->set('last-password-confirm', $this->timeFactory->getTime()); + } else { $lastConfirm = (int) $this->session->get('last-password-confirm'); // TODO: confirm excludedUserBackEnds can go away and remove it if (!isset($this->excludedUserBackEnds[$backendClassName]) && $lastConfirm < ($this->timeFactory->getTime() - (30 * 60 + 15))) { // allow 15 seconds delay @@ -116,23 +112,22 @@ class PasswordConfirmationMiddleware extends Middleware { } } - /** - * @template T - * - * @param ReflectionMethod $reflectionMethod - * @param string $annotationName - * @param class-string<T> $attributeClass - * @return boolean - */ - protected function hasAnnotationOrAttribute(ReflectionMethod $reflectionMethod, string $annotationName, string $attributeClass): bool { - if (!empty($reflectionMethod->getAttributes($attributeClass))) { + private function needsPasswordConfirmation(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + if (!empty($attributes)) { return true; } - if ($this->reflector->hasAnnotation($annotationName)) { + if ($this->reflector->hasAnnotation('PasswordConfirmationRequired')) { + $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . 'PasswordConfirmationRequired' . ' annotation and should use the #[PasswordConfirmationRequired] attribute instead'); return true; } return false; } + + private function isPasswordConfirmationStrict(ReflectionMethod $reflectionMethod): bool { + $attributes = $reflectionMethod->getAttributes(PasswordConfirmationRequired::class); + return !empty($attributes) && ($attributes[0]->newInstance()->getStrict()); + } } |