Search code examples
phplaminas

laminas authentication - redirect to login page when using a listeneraggregate


I am migrating an app from ZF2/3 to Laminas. I have moved the authentication from the Module.php bootstrap, to an aggregatelistener, and would like to redirect to a login page if the user is not logged in for certain routes. This worked fine originally, but i am having trouble after the migration - it is now redirecting 20 to 30 times and causing a "page is not redirecting properly" error.

My Module.php onBoostrap method:

class Module implements ConfigProviderInterface
{
    /**
     * @param $event
     */
    public function onBootstrap(MvcEvent $event)
    {
        /** @var ApplicationInterface $application*/
        $application = $event->getApplication();

        /** @var TemplateMapResolver $templateMapResolver */
        $templateMapResolver = $application->getServiceManager()->get(
            'ViewTemplateMapResolver'
        );

        // Create and register layout listener
        $listener = new LayoutListener($templateMapResolver);
        $listener->attach($application->getEventManager());

        //Create and register Authentication listener
        $authenticationListener = new AuthenticationListener();
        $authenticationListener->attach($application->getEventManager());
    }
}

The AuthenticationListener class:

class AuthenticationListener extends AbstractListenerAggregate
{
    /**
     * @param EventManagerInterface $events
     * @param int $priority
     */
    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $this->listeners[] = $events->attach(
            MvcEvent::EVENT_DISPATCH,
            [$this, 'userHasAuthentication']
        );
    }

    /**
     * @param MvcEvent $event
     */
    public function userHasAuthentication(MvcEvent $event)
    {
        $authenticationService   = $event->getApplication()->getServiceManager()->get(AuthenticationService::class);
        if($authenticationService->hasIdentity() === false){
            $event->stopPropagation(true);
            // ? how to redirect
        }
        return true;
    }
}

I have tried the following approaches to redirecting, and these still end up with the "not redirecting properly" result. Inside AuthenticationListener::userHasAuthentication:

if($authenticationService->hasIdentity() === false){
    $event->stopPropagation(true);
    /**@var AbstractActionController $target*/
    $target = $event->getTarget();
    return $target->redirect()->toRoute('auth.login');     
}

...or...

if($authenticationService->hasIdentity() === false){
    $event->stopPropagation(true);
    /**@var AbstractActionController $target*/
    $target = $event->getTarget();
    $response = $target->getResponse();
    $response->getHeaders()->addHeaderLine('Location', '/login');
    $response->setStatusCode(403);
    $response->sendHeaders();
    return $response;     
}

What is the correct way of achieving this?


Solution

  • I think you get redirection loop, cause this listener is also triggered on /login page. You have to check the current route before redirecting.

    public function userHasAuthentication(MvcEvent $e)
    {
        $routeMatch = $e->getRouteMatch();
        if ($routeMatch) {
            $routeName = $routeMatch->getMatchedRouteName();
            if ($routeName !== 'login' && $routeMatch->getParam('loginRequired',true)) {
                $auth = $e->getApplication()->getServiceManager()->get(AuthenticationServiceInterface::class);
                if ($auth->hasIdentity() === false) {
                    $response = new \Laminas\Http\PhpEnvironment\Response();
                    $response->getHeaders()->addHeaderLine('Location', "/login");
                    $response->setStatusCode(302);
                    return $response;
                }
            }
        }
    }
    

    Adding condition on route param 'loginRequired' allows you disable redirection for chosen paths adding 'loginRequired'=>false in 'defaults' section in route config.

    BTW, if you use higher listener priority, or attach it do MvcEvent::ROUTE, you can display login page on every path by changing route match

    public function userHasAuthentication(MvcEvent $e)
    {
        $routeMatch = $e->getRouteMatch();
        if ($routeMatch) {
            $routeName = $routeMatch->getMatchedRouteName();
            if ($routeName !== 'login'
                && $routeName !== 'logout'
                && $routeMatch->getParam('loginRequired', true) !== false
            ) {
                $auth = $e->getApplication()->getServiceManager()->get(AuthenticationServiceInterface::class);
                if ($auth->hasIdentity() === false) {
                    $routeMatch->setParam('controller', LoginController::class);
                    $routeMatch->setParam('action', 'login');
                    if ($routeName !== 'home') {
                        $e->getResponse()->setStatusCode(401);
                    }
                }
            }
        }
    }
    

    On loginAction add

    if ($this->auth->hasIdentity()) {
        $this->checkTourDismiss($this->auth->getIdentity());
        if (isset($_SERVER['REQUEST_URI'])
            && !in_array($_SERVER['REQUEST_URI'], ['/', '/login'])
        ) {
            $this->redirect()->toUrl($_SERVER['REQUEST_URI']);
        } else {
                $this->redirect()->toRoute('home');    
        }
    }
    

    in the end, so after logging in user stays on URL he started with.