Skip to content

Commit 7c8045f

Browse files
committed
TASK: Add RedirectMiddleware and make Flow/Neos 7 compatible
1 parent 313e1b1 commit 7c8045f

6 files changed

Lines changed: 62 additions & 59 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Yeebase\TwoFactorAuthentication\Error;
4+
5+
/**
6+
* This Exception get thrown inside the authentication provider to trigger a redirect by the middleware.
7+
* This is to redirect the user to the form for the second factor.
8+
*/
9+
class SecondFactorLoginException extends \Exception
10+
{
11+
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Yeebase\TwoFactorAuthentication\Error;
4+
5+
/**
6+
* This Exception get thrown inside the authentication provider to trigger a redirect by the middleware.
7+
* This is to redirect the user to the form for the second factor.
8+
*/
9+
class SecondFactorSetupException extends \Exception
10+
{
11+
12+
}
Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
namespace Yeebase\TwoFactorAuthentication\Http;
44

55
use Neos\Flow\Annotations as Flow;
6-
use Neos\Flow\Http\Component\ComponentChain;
7-
use Neos\Flow\Http\Component\ComponentContext;
8-
use Neos\Flow\Http\Component\ComponentInterface;
9-
use Neos\Flow\Http\Request as HttpRequest;
106
use Neos\Flow\Mvc\ActionRequest;
117
use Neos\Flow\Mvc\Routing\UriBuilder;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\MiddlewareInterface;
11+
use Psr\Http\Server\RequestHandlerInterface;
12+
use GuzzleHttp\Psr7\Response;
13+
use Yeebase\TwoFactorAuthentication\Error\SecondFactorLoginException;
14+
use Yeebase\TwoFactorAuthentication\Error\SecondFactorSetupException;
1215

1316
/**
1417
* A HTTP component that redirects to the configured 2FA login/setup routes if requested
1518
*/
16-
final class RedirectComponent implements ComponentInterface
19+
final class RedirectMiddleware implements MiddlewareInterface
1720
{
1821
public const REDIRECT_LOGIN = 'login';
1922
public const REDIRECT_SETUP = 'setup';
@@ -30,45 +33,46 @@ final class RedirectComponent implements ComponentInterface
3033
*/
3134
protected $setupRouteValue;
3235

33-
public function handle(ComponentContext $componentContext)
36+
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
3437
{
35-
$redirectTarget = $componentContext->getParameter(static::class, 'redirect');
36-
if ($redirectTarget === null) {
37-
return;
38-
}
39-
if ($redirectTarget === self::REDIRECT_LOGIN) {
40-
$this->redirectToLogin($componentContext);
41-
} elseif ($redirectTarget === self::REDIRECT_SETUP) {
42-
$this->redirectToSetup($componentContext);
43-
} else {
44-
throw new \RuntimeException(sprintf('Invalid redirect target "%s"', $redirectTarget), 1568189192);
38+
try {
39+
$response = $next->handle($request);
40+
} catch (\Exception $exception) {
41+
if ($exception instanceof SecondFactorLoginException || $exception->getPrevious() instanceof SecondFactorLoginException) {
42+
return $this->redirectToLogin($request);
43+
} elseif ($exception instanceof SecondFactorSetupException || $exception->getPrevious() instanceof SecondFactorSetupException) {
44+
return $this->redirectToSetup($request);
45+
} else {
46+
throw $exception;
47+
}
4548
}
49+
return $response;
4650
}
4751

4852
/**
4953
* Triggers a redirect to the 2FA login route configured at routes.login or throws an exception if the configuration is missing/incorrect
5054
*/
51-
private function redirectToLogin(ComponentContext $componentContext): void
55+
private function redirectToLogin(ServerRequestInterface $request): ResponseInterface
5256
{
5357
try {
5458
$this->validateRouteValues($this->loginRouteValues);
5559
} catch (\InvalidArgumentException $exception) {
5660
throw new \RuntimeException('Missing/invalid routes.login configuration: ' . $exception->getMessage(), 1550660144, $exception);
5761
}
58-
$this->redirect($componentContext, $this->loginRouteValues);
62+
return $this->redirect($request, $this->loginRouteValues);
5963
}
6064

6165
/**
6266
* Triggers a redirect to the 2FA setup route configured at routes.setup or throws an exception if the configuration is missing/incorrect
6367
*/
64-
private function redirectToSetup(ComponentContext $componentContext): void
68+
private function redirectToSetup(ServerRequestInterface $request): ResponseInterface
6569
{
6670
try {
6771
$this->validateRouteValues($this->setupRouteValue);
6872
} catch (\InvalidArgumentException $exception) {
6973
throw new \RuntimeException('Missing/invalid routes.setup configuration: ' . $exception->getMessage(), 1550660178, $exception);
7074
}
71-
$this->redirect($componentContext, $this->setupRouteValue);
75+
return $this->redirect($request, $this->setupRouteValue);
7276
}
7377

7478
private function validateRouteValues(array $routeValues): void
@@ -81,16 +85,13 @@ private function validateRouteValues(array $routeValues): void
8185
}
8286
}
8387

84-
private function redirect(ComponentContext $componentContext, array $routeValues): void
88+
private function redirect(ServerRequestInterface $httpRequest, array $routeValues): ResponseInterface
8589
{
86-
/** @var HttpRequest $httpRequest */
87-
$httpRequest = $componentContext->getHttpRequest();
88-
$actionRequest = new ActionRequest($httpRequest);
90+
$actionRequest = ActionRequest::fromHttpRequest($httpRequest);
8991
$uriBuilder = new UriBuilder();
9092
$uriBuilder->setRequest($actionRequest);
9193
$redirectUrl = $uriBuilder->setCreateAbsoluteUri(true)->setFormat('html')->build($routeValues);
9294

93-
$componentContext->replaceHttpResponse($componentContext->getHttpResponse()->withStatus(303)->withHeader('Location', $redirectUrl));
94-
$componentContext->setParameter(ComponentChain::class, 'cancel', true);
95+
return (new Response())->withStatus(303)->withHeader('Location', $redirectUrl);
9596
}
9697
}

Classes/Security/Authentication/Provider/TwoFactorAuthenticationProvider.php

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
use Neos\Flow\Session\SessionManagerInterface;
1515
use Neos\Utility\Exception\PropertyNotAccessibleException;
1616
use Neos\Utility\ObjectAccess;
17+
use Yeebase\TwoFactorAuthentication\Error\SecondFactorSetupException;
18+
use Yeebase\TwoFactorAuthentication\Error\SecondFactorLoginException;
1719
use Yeebase\TwoFactorAuthentication\Http\RedirectComponent;
20+
use Yeebase\TwoFactorAuthentication\Http\RedirectMiddleware;
1821
use Yeebase\TwoFactorAuthentication\Security\Authentication\Token\OtpToken;
1922
use Yeebase\TwoFactorAuthentication\Service\TwoFactorAuthenticationService;
2023

@@ -55,7 +58,7 @@ public function getTokenClassNames(): array
5558

5659
/**
5760
* @param TokenInterface $authenticationToken
58-
* @throws AuthenticationRequiredException | UnsupportedAuthenticationTokenException
61+
* @throws AuthenticationRequiredException | UnsupportedAuthenticationTokenException | SecondFactorLoginException | SecondFactorSetupException
5962
*/
6063
public function authenticate(TokenInterface $authenticationToken): void
6164
{
@@ -68,8 +71,7 @@ public function authenticate(TokenInterface $authenticationToken): void
6871
}
6972
if ($this->twoFactorAuthenticationService->isTwoFactorAuthenticationEnabledFor($account)) {
7073
if (!$authenticationToken->hasOtp()) {
71-
$this->requestRedirect(RedirectComponent::REDIRECT_LOGIN);
72-
return;
74+
throw new SecondFactorLoginException();
7375
}
7476
if ($this->twoFactorAuthenticationService->validateOtp($account, $authenticationToken->getOtp())) {
7577
/** @noinspection PhpUnhandledExceptionInspection */
@@ -82,33 +84,11 @@ public function authenticate(TokenInterface $authenticationToken): void
8284
return;
8385
}
8486
if ($this->requireTwoFactorAuthentication) {
85-
$this->requestRedirect(RedirectComponent::REDIRECT_SETUP);
87+
throw new SecondFactorSetupException();
8688
} else {
8789
/** @noinspection PhpUnhandledExceptionInspection */
8890
$authenticationToken->setAuthenticationStatus(TokenInterface::AUTHENTICATION_SUCCESSFUL);
8991
$authenticationToken->setAccount($account);
9092
}
9193
}
92-
93-
/**
94-
* Triggers a redirect by setting the corresponding HTTP component parameter for the @see RedirectComponent to pick up
95-
*
96-
* @param string $target one of the RedirectComponent::REDIRECT_* constants
97-
*/
98-
private function requestRedirect(string $target): void
99-
{
100-
$requestHandler = $this->bootstrap->getActiveRequestHandler();
101-
if (!$requestHandler instanceof HttpRequestHandlerInterface) {
102-
throw new \RuntimeException('This provider only supports HTTP requests', 1549985779);
103-
}
104-
try {
105-
$componentContext = ObjectAccess::getProperty($requestHandler, 'componentContext', true);
106-
} catch (PropertyNotAccessibleException $e) {
107-
throw new \RuntimeException('Faild to extract ComponentContext from RequestHandler', 1568188386, $e);
108-
}
109-
if (!$componentContext instanceof ComponentContext) {
110-
throw new \RuntimeException('Faild to extract ComponentContext from RequestHandler', 1568188387);
111-
}
112-
$componentContext->setParameter(RedirectComponent::class, 'redirect', $target);
113-
}
11494
}

Configuration/Settings.yaml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,10 @@ Yeebase:
2929
Neos:
3030
Flow:
3131
http:
32-
chain:
33-
'process':
34-
chain:
35-
'Yeebase.TwoFactorAuthentication:Redirect':
36-
position: 'after dispatching'
37-
component: 'Yeebase\TwoFactorAuthentication\Http\RedirectComponent'
32+
middlewares:
33+
'Yeebase.TwoFactorAuthentication:Redirect':
34+
position: 'start'
35+
middleware: 'Yeebase\TwoFactorAuthentication\Http\RedirectMiddleware'
3836
persistence:
3937
doctrine:
4038
migrations:

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "Two-Factor-Authentication (2FA) for Neos Flow",
55
"license": "MIT",
66
"require": {
7-
"neos/flow": "^6.0",
7+
"neos/flow": "^6.0 || ^7.0",
88
"pragmarx/google2fa": "^4.0",
99
"bacon/bacon-qr-code": "^2.0"
1010
},

0 commit comments

Comments
 (0)