diff --git a/Classes/Error/SecondFactorLoginException.php b/Classes/Error/SecondFactorLoginException.php new file mode 100644 index 0000000..b3584ed --- /dev/null +++ b/Classes/Error/SecondFactorLoginException.php @@ -0,0 +1,12 @@ +getParameter(static::class, 'redirect'); - if ($redirectTarget === null) { - return; - } - if ($redirectTarget === self::REDIRECT_LOGIN) { - $this->redirectToLogin($componentContext); - } elseif ($redirectTarget === self::REDIRECT_SETUP) { - $this->redirectToSetup($componentContext); - } else { - throw new \RuntimeException(sprintf('Invalid redirect target "%s"', $redirectTarget), 1568189192); + try { + $response = $next->handle($request); + } catch (\Exception $exception) { + if ($exception instanceof SecondFactorLoginException || $exception->getPrevious() instanceof SecondFactorLoginException) { + return $this->redirectToLogin($request); + } elseif ($exception instanceof SecondFactorSetupException || $exception->getPrevious() instanceof SecondFactorSetupException) { + return $this->redirectToSetup($request); + } else { + throw $exception; + } } + return $response; } /** * Triggers a redirect to the 2FA login route configured at routes.login or throws an exception if the configuration is missing/incorrect */ - private function redirectToLogin(ComponentContext $componentContext): void + private function redirectToLogin(ServerRequestInterface $request): ResponseInterface { try { $this->validateRouteValues($this->loginRouteValues); } catch (\InvalidArgumentException $exception) { throw new \RuntimeException('Missing/invalid routes.login configuration: ' . $exception->getMessage(), 1550660144, $exception); } - $this->redirect($componentContext, $this->loginRouteValues); + return $this->redirect($request, $this->loginRouteValues); } /** * Triggers a redirect to the 2FA setup route configured at routes.setup or throws an exception if the configuration is missing/incorrect */ - private function redirectToSetup(ComponentContext $componentContext): void + private function redirectToSetup(ServerRequestInterface $request): ResponseInterface { try { $this->validateRouteValues($this->setupRouteValue); } catch (\InvalidArgumentException $exception) { throw new \RuntimeException('Missing/invalid routes.setup configuration: ' . $exception->getMessage(), 1550660178, $exception); } - $this->redirect($componentContext, $this->setupRouteValue); + return $this->redirect($request, $this->setupRouteValue); } private function validateRouteValues(array $routeValues): void @@ -81,16 +85,13 @@ private function validateRouteValues(array $routeValues): void } } - private function redirect(ComponentContext $componentContext, array $routeValues): void + private function redirect(ServerRequestInterface $httpRequest, array $routeValues): ResponseInterface { - /** @var HttpRequest $httpRequest */ - $httpRequest = $componentContext->getHttpRequest(); - $actionRequest = new ActionRequest($httpRequest); + $actionRequest = ActionRequest::fromHttpRequest($httpRequest); $uriBuilder = new UriBuilder(); $uriBuilder->setRequest($actionRequest); $redirectUrl = $uriBuilder->setCreateAbsoluteUri(true)->setFormat('html')->build($routeValues); - $componentContext->replaceHttpResponse($componentContext->getHttpResponse()->withStatus(303)->withHeader('Location', $redirectUrl)); - $componentContext->setParameter(ComponentChain::class, 'cancel', true); + return (new Response())->withStatus(303)->withHeader('Location', $redirectUrl); } } diff --git a/Classes/Security/Authentication/Provider/TwoFactorAuthenticationProvider.php b/Classes/Security/Authentication/Provider/TwoFactorAuthenticationProvider.php index 821a6b4..efa174c 100644 --- a/Classes/Security/Authentication/Provider/TwoFactorAuthenticationProvider.php +++ b/Classes/Security/Authentication/Provider/TwoFactorAuthenticationProvider.php @@ -14,7 +14,10 @@ use Neos\Flow\Session\SessionManagerInterface; use Neos\Utility\Exception\PropertyNotAccessibleException; use Neos\Utility\ObjectAccess; +use Yeebase\TwoFactorAuthentication\Error\SecondFactorSetupException; +use Yeebase\TwoFactorAuthentication\Error\SecondFactorLoginException; use Yeebase\TwoFactorAuthentication\Http\RedirectComponent; +use Yeebase\TwoFactorAuthentication\Http\RedirectMiddleware; use Yeebase\TwoFactorAuthentication\Security\Authentication\Token\OtpToken; use Yeebase\TwoFactorAuthentication\Service\TwoFactorAuthenticationService; @@ -55,7 +58,7 @@ public function getTokenClassNames(): array /** * @param TokenInterface $authenticationToken - * @throws AuthenticationRequiredException | UnsupportedAuthenticationTokenException + * @throws AuthenticationRequiredException | UnsupportedAuthenticationTokenException | SecondFactorLoginException | SecondFactorSetupException */ public function authenticate(TokenInterface $authenticationToken): void { @@ -68,8 +71,7 @@ public function authenticate(TokenInterface $authenticationToken): void } if ($this->twoFactorAuthenticationService->isTwoFactorAuthenticationEnabledFor($account)) { if (!$authenticationToken->hasOtp()) { - $this->requestRedirect(RedirectComponent::REDIRECT_LOGIN); - return; + throw new SecondFactorLoginException(); } if ($this->twoFactorAuthenticationService->validateOtp($account, $authenticationToken->getOtp())) { /** @noinspection PhpUnhandledExceptionInspection */ @@ -82,33 +84,11 @@ public function authenticate(TokenInterface $authenticationToken): void return; } if ($this->requireTwoFactorAuthentication) { - $this->requestRedirect(RedirectComponent::REDIRECT_SETUP); + throw new SecondFactorSetupException(); } else { /** @noinspection PhpUnhandledExceptionInspection */ $authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL); $authenticationToken->setAccount($account); } } - - /** - * Triggers a redirect by setting the corresponding HTTP component parameter for the @see RedirectComponent to pick up - * - * @param string $target one of the RedirectComponent::REDIRECT_* constants - */ - private function requestRedirect(string $target): void - { - $requestHandler = $this->bootstrap->getActiveRequestHandler(); - if (!$requestHandler instanceof HttpRequestHandlerInterface) { - throw new \RuntimeException('This provider only supports HTTP requests', 1549985779); - } - try { - $componentContext = ObjectAccess::getProperty($requestHandler, 'componentContext', true); - } catch (PropertyNotAccessibleException $e) { - throw new \RuntimeException('Faild to extract ComponentContext from RequestHandler', 1568188386, $e); - } - if (!$componentContext instanceof ComponentContext) { - throw new \RuntimeException('Faild to extract ComponentContext from RequestHandler', 1568188387); - } - $componentContext->setParameter(RedirectComponent::class, 'redirect', $target); - } } diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index e62d01c..ae6a13f 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -29,12 +29,10 @@ Yeebase: Neos: Flow: http: - chain: - 'process': - chain: - 'Yeebase.TwoFactorAuthentication:Redirect': - position: 'after dispatching' - component: 'Yeebase\TwoFactorAuthentication\Http\RedirectComponent' + middlewares: + 'Yeebase.TwoFactorAuthentication:Redirect': + position: 'start' + middleware: 'Yeebase\TwoFactorAuthentication\Http\RedirectMiddleware' persistence: doctrine: migrations: diff --git a/composer.json b/composer.json index 323d66b..473968f 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,7 @@ "description": "Two-Factor-Authentication (2FA) for Neos Flow", "license": "MIT", "require": { - "neos/flow": "^6.0", + "neos/flow": "^6.0 || ^7.0 || ^8.0", "pragmarx/google2fa": "^4.0", "bacon/bacon-qr-code": "^2.0" },