Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions Classes/Http/RedirectMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Yeebase\TwoFactorAuthentication\Http;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Routing\UriBuilder;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use GuzzleHttp\Psr7\Response;

/**
* A HTTP component that redirects to the configured 2FA login/setup routes if requested
*/
final class RedirectMiddleware implements MiddlewareInterface
{
public const REDIRECT_LOGIN = 'login';
public const REDIRECT_SETUP = 'setup';

/**
* @Flow\InjectConfiguration(path="routes.login")
* @var array
*/
protected $loginRouteValues;

/**
* @Flow\InjectConfiguration(path="routes.setup")
* @var array
*/
protected $setupRouteValue;

public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
$response = $next->handle($request);
// TODO: Find a better solution to communicate from TokenProvider to this middleware
$redirectTarget = $response->getHeaderLine(static::class . '.redirect');
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See neos/flow-development-collection#2019 (comment)

Though we do not even have access to the ActionResponse in the TokenProvider, so this might become a bit harder to achieve than anticipated. If anyone has a good idea, just shoot.
I'm still thinking if it wouldn't make more sense to use the entryPoint configuration for the login redirect and use a middleware that checks for the setup redirect.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albe @johannessteu
I think it could work like that:
master...gerdemann:Neos7

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using exceptions for control flow is a bit quirky because of the overhead of throwing/catching exceptions (i.e. they have to create a full stack trace).
But in this case I also think that it's the nicer solution – and one top-level exception shouldn't do any harm

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@albe Do you want to adapt your PR or should I create a new one? 😄

Copy link
Collaborator

@bwaidelich bwaidelich Apr 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gerdemann btw I think your solution could be simplified to

    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
    {
        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;
    }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, I have now adjusted this and created a PR. It doesn't matter to me whether this PR is adjusted or mine is merged. 😃
#8

if ($redirectTarget === null) {
return $response;
}
if ($redirectTarget === self::REDIRECT_LOGIN) {
return $this->redirectToLogin($request);
} elseif ($redirectTarget === self::REDIRECT_SETUP) {
return $this->redirectToSetup($request);
} else {
throw new \RuntimeException(sprintf('Invalid redirect target "%s"', $redirectTarget), 1568189192);
}
}

/**
* 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(ServerRequestInterface $request): ResponseInterface
{
try {
$this->validateRouteValues($this->loginRouteValues);
} catch (\InvalidArgumentException $exception) {
throw new \RuntimeException('Missing/invalid routes.login configuration: ' . $exception->getMessage(), 1550660144, $exception);
}
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(ServerRequestInterface $request): ResponseInterface
{
try {
$this->validateRouteValues($this->setupRouteValue);
} catch (\InvalidArgumentException $exception) {
throw new \RuntimeException('Missing/invalid routes.setup configuration: ' . $exception->getMessage(), 1550660178, $exception);
}
return $this->redirect($request, $this->setupRouteValue);
}

private function validateRouteValues(array $routeValues): void
{
$requiredRouteValues = ['@package', '@controller', '@action'];
foreach ($requiredRouteValues as $routeValue) {
if (!array_key_exists($routeValue, $routeValues)) {
throw new \InvalidArgumentException(sprintf('Missing "%s" route value', $routeValue), 1550660039);
}
}
}

private function redirect(ServerRequestInterface $httpRequest, array $routeValues): ResponseInterface
{
$actionRequest = ActionRequest::fromHttpRequest($httpRequest);
$uriBuilder = new UriBuilder();
$uriBuilder->setRequest($actionRequest);
$redirectUrl = $uriBuilder->setCreateAbsoluteUri(true)->setFormat('html')->build($routeValues);

return (new Response())->withStatus(303)->withHeader('Location', $redirectUrl);
}
}