Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TASK: Add RedirectMiddleware and make Flow/Neos 7 compatible #8

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions Classes/Error/SecondFactorLoginException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Yeebase\TwoFactorAuthentication\Error;

/**
* This Exception get thrown inside the authentication provider to trigger a redirect by the middleware.
* This is to redirect the user to the form for the second factor.
*/
class SecondFactorLoginException extends \Exception
{

}
12 changes: 12 additions & 0 deletions Classes/Error/SecondFactorSetupException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Yeebase\TwoFactorAuthentication\Error;

/**
* This Exception get thrown inside the authentication provider to trigger a redirect by the middleware.
* This is to redirect the user to the form for the second factor.
*/
class SecondFactorSetupException extends \Exception
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
namespace Yeebase\TwoFactorAuthentication\Http;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Component\ComponentChain;
use Neos\Flow\Http\Component\ComponentContext;
use Neos\Flow\Http\Component\ComponentInterface;
use Neos\Flow\Http\Request as HttpRequest;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use GuzzleHttp\Psr7\Response;
use Yeebase\TwoFactorAuthentication\Error\SecondFactorLoginException;
use Yeebase\TwoFactorAuthentication\Error\SecondFactorSetupException;

/**
* A HTTP component that redirects to the configured 2FA login/setup routes if requested
*/
final class RedirectComponent implements ComponentInterface
final class RedirectMiddleware implements MiddlewareInterface
{
public const REDIRECT_LOGIN = 'login';
public const REDIRECT_SETUP = 'setup';
Expand All @@ -30,45 +33,46 @@ final class RedirectComponent implements ComponentInterface
*/
protected $setupRouteValue;

public function handle(ComponentContext $componentContext)
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
$redirectTarget = $componentContext->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
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
{
Expand All @@ -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 */
Expand All @@ -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);
}
}
10 changes: 4 additions & 6 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down