mirror of
https://github.com/nextcloud/server.git
synced 2026-03-27 04:43:20 -04:00
SSO backends like SAML and OIDC tried a trick to suppress password confirmations as they are not possible by design. At least for SAML it was not reliable when existing user backends where used as user repositories. Now we are setting a special scope with the token, and also make sure that the scope is taken over when tokens are regenerated. Signed-off-by: Arthur Schiwon <blizzz@arthur-schiwon.de>
120 lines
3.8 KiB
PHP
120 lines
3.8 KiB
PHP
<?php
|
|
/**
|
|
* 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 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\ISession;
|
|
use OCP\IUserSession;
|
|
use OCP\Session\Exceptions\SessionNotAvailableException;
|
|
use OCP\User\Backend\IPasswordConfirmationBackend;
|
|
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;
|
|
|
|
/**
|
|
* 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,
|
|
) {
|
|
$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) {
|
|
$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;
|
|
}
|
|
}
|
|
|
|
$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['sso-based-login']) && $scope['sso-based-login'] === true) {
|
|
// Users logging in from SSO backends cannot confirm their password by design
|
|
return;
|
|
}
|
|
|
|
$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))) {
|
|
return true;
|
|
}
|
|
|
|
if ($this->reflector->hasAnnotation($annotationName)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|