[SOLVED] Frontend login: Redirect to login form


(Leif Hartmann) #1

Hi,

I have successfully implemented a frontend login, based on the information from Flowpack.Neos.FrontendLogin.

I have two issues though:

  1. If someone accesses nodes that required a login, i.e. via /members-area/, Neos responds with “#1430218623: The requested node does not exist or isn’t accessible to the current user”
    Is there some way to catch this exception and redirect the user to the login page instead?
    I already tried to solve this problem using entryPoint WebRedirect, as mentioned in the Flow docs, but it seems like this does not work with the Neos NodeController.

  2. The following is only useful, if my first problem is solved: The menu does not contain the protected menu item “Members Area”, which is fine in some use cases. I however would like the user to see this menu item. Is there a way to achieve this?

Regards
Leif


(Bastian Waidelich) #2

Hi Leif,

Inaccessible document nodes are handled just like 404s for security reasons (information disclosure).
I would suggest that you create a new document node type for the members area entry page and make that one accessible.
In fusion you can do something like that

prototype(Your.Site:MembersArea.Document) < prototype(Your.Site:DefaultPage) {
    body {
        @cache {
            mode = 'uncached'
            context {
                1 = 'site'
                2 = 'node'
                3 = 'documentNode'
            }
        }

        content.main = Neos.Fusion:Case {
            authenticated {
                condition = ${Security.hasRole('Your.Site:FeUserRole') && !node.context.inBackend}
                renderer = 'Your.Site:MemberArea.Authenticated.Document'
            }
            notAuthenticated {
                @position = 'end'
                condition = true
                renderer = 'Your.Site:MemberArea.LoginForm.Document'
            }
        }
    }
}

(untested)


(Leif Hartmann) #3

Hi Bastian,

thanks for your suggestion. In the meantime I came up with a solution using the beforeControllerInvocation signal. Since I have several pages unter /members-area/ and the login in page is configurable by backend users, I think this is the better fit for my case.

Since others might have the same problem, here is what I did:

Using the signal in the Package class:

class Package extends BasePackage
{
    public function boot(Bootstrap $bootstrap)
    {
        $dispatcher = $bootstrap->getSignalSlotDispatcher();
        $dispatcher->connect(Dispatcher::class, 'beforeControllerInvocation', AuthenticationListener::class, 'handleLoginRedirect');
    }
}

The AuthenticationListener:

<?php

namespace My\Site\Namespace\Security;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\Response;
use Neos\Flow\Log\SystemLoggerInterface;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Controller\ControllerInterface;
use Neos\Flow\Mvc\Exception\StopActionException;
use Neos\Flow\Mvc\RequestInterface;
use Neos\Flow\Mvc\ResponseInterface;
use Neos\Flow\Security\Context;

/**
 * This listener redirects to the login page, if the user is not authenticated and tries to access a protected URI.
 */
class AuthenticationListener
{
    /**
     * @Flow\Inject()
     * @var SystemLoggerInterface
     */
    protected $logger;

    /**
     * @Flow\Inject()
     * @var Context
     */
    protected $securityContext;

    /**
     * @Flow\InjectConfiguration("authentication.protectedUris")
     * @var array
     */
    protected $protectedUris = [];

    /**
     * @Flow\InjectConfiguration("authentication.loginUri")
     * @var string
     */
    protected $loginUri = null;

    public function handleLoginRedirect(RequestInterface $request, ResponseInterface $response, ControllerInterface $controller)
    {
        if (!$response instanceof Response || !$request instanceof ActionRequest) {
            return;
        }

        if (!$this->securityContext->canBeInitialized()) {
            return;
        }

        if ($this->loginUri === null) {
            $this->logger->log("[AuthenticationListener] loginUri not configured. Please configure it using setting [My.Site.Namespace.authentication.loginUri]", LOG_WARNING);
            return;
        }

        if (!is_array($this->protectedUris)) {
            $this->logger->log("[AuthenticationListener] protectedUris not configured. Please configure it using setting [My.Site.Namespace.authentication.protectedUris]", LOG_WARNING);
            return;
        }

        $path = $request->getHttpRequest()->getUri()->getPath();
        $isSecuredArea = false;

        foreach($this->protectedUris as $pattern) {
            if (preg_match($pattern, $path)) {
                $isSecuredArea = true;
                break;
            }
        }

        if (!$isSecuredArea) {
            return;
        }

        $account = $this->securityContext->getAccount();

        if ($account) {
            $this->logger->log("[AuthenticationListener] We are authenticated with account [{$account->getAccountIdentifier()}]");
        } else {
            $this->logger->log("[AuthenticationListener] Not authenticated, redirecting to login page at [{$this->loginUri}]");

            $loginUri = $this->loginUri.'?redirectAfterLogin='.urlencode($path);

            $escapedUri = htmlentities($loginUri, ENT_QUOTES, 'utf-8');
            $response->setContent('<html><head><meta http-equiv="refresh" content="' . intval(0) . ';url=' . $escapedUri . '"/></head></html>');
            $response->setStatus(302);
            $response->setHeader('Location', $loginUri);
            $response->setHeader('X-Redirect-Reason', 'NotAuthenticated');

            $request->setDispatched(true);

            throw new StopActionException();
        }
    }
}

The AuthenticationController of the plugin uses the GET parameter redirectAfterLogin to redirect to the correct URI after logging in successfully.

The downside is that you cannot configure the protected URIs in the backend. This is done in the Settings.yaml:

My:
  Site:
    Namespace:
      authentication:
        loginUri: '/login'
        protectedUris:
          - '@^/members-area/?@'
          - '@^/some-other-protected-page/?@'

I would like your opinion on this solution though!

Regards
Leif