diff options
Diffstat (limited to 'lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php')
-rw-r--r-- | lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php | 149 |
1 files changed, 77 insertions, 72 deletions
diff --git a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php index 351f47ea924..0facbffe504 100644 --- a/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/PasswordConfirmationMiddleware.php @@ -1,116 +1,121 @@ <?php + /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ 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; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Authentication\Exceptions\ExpiredTokenException; +use OCP\Authentication\Exceptions\InvalidTokenException; +use OCP\Authentication\Exceptions\WipeTokenException; +use OCP\Authentication\Token\IToken; +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 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) { - $this->reflector = $reflector; - $this->session = $session; - $this->userSession = $userSession; - $this->timeFactory = $timeFactory; + public function __construct( + private ControllerMethodReflector $reflector, + private ISession $session, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private IProvider $tokenProvider, + private readonly LoggerInterface $logger, + private readonly IRequest $request, + private readonly Manager $userManager, + ) { } /** - * @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; + } + + $user = $this->userSession->getUser(); + $backendClassName = ''; + if ($user !== null) { + $backend = $user->getBackend(); + if ($backend instanceof IPasswordConfirmationBackend) { + if (!$backend->canConfirmPassword($user->getUID())) { + 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[IToken::SCOPE_SKIP_PASSWORD_VALIDATION]) && $scope[IToken::SCOPE_SKIP_PASSWORD_VALIDATION] === true) { + // Users logging in from SSO backends cannot confirm their password by design + return; + } - $backendClassName = $user->getBackendClassName(); + if ($this->isPasswordConfirmationStrict($reflectionMethod)) { + $authHeader = $this->request->getHeader('Authorization'); + if (!str_starts_with(strtolower($authHeader), 'basic ')) { + throw new NotConfirmedException('Required authorization header missing'); + } + [, $password] = explode(':', base64_decode(substr($authHeader, 6)), 2); + $loginName = $this->session->get('loginname'); + $loginResult = $this->userManager->checkPassword($loginName, $password); + if ($loginResult === false) { + throw new NotConfirmedException(); } - $lastConfirm = (int) $this->session->get('last-password-confirm'); - // we can't check the password against a SAML backend, so skip password confirmation in this case + $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 throw new NotConfirmedException(); } } } - /** - * @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()); + } } |