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, private readonly LoggerInterface $logger, ) { $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[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; } $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 $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)) { $this->logger->debug($reflectionMethod->getDeclaringClass()->getName() . '::' . $reflectionMethod->getName() . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead'); return true; } return false; } }