Search code examples
phphttp-redirectzend-framework2aclforward

zf2 acl forwarding to login form


I would like to forward non-authenticated users (guest) to a login form.

When working with redirect, I need to redirect the guest users from the page they're looking for to the login page; and then redirect them back again.

Example:

(guest visits) mySite/controller/action?var1=xxx&var2=yyy

(AclService redirects) mySite/login

(AuthService redirects) mySite/controller/action?var1=xxx&var2=yyy

And all this can only work (I guess) using session variables.

My idea instead, is to forward user to mySite/login. When the authentication is successfull, the only thing I need to do is to redirect to the current URL. The advantage of this behaviour is that if the user clicks the browser's back button, the page remains the same (mySite/controller/action?var1=xxx&var2=yyy).

Here my code:

in module.php

public function onBootstrap(MvcEvent $e){

$app = $e->getApplication();
$sm  = $app->getServiceManager();
$acl = $sm->get('AclService');
$e -> getApplication()-> getEventManager()->getSharedManager()->attach('Zend\Mvc\Controller\AbstractActionController',MvcEvent::EVENT_DISPATCH,array($acl, 'checkAcl'),-10);

}

in my AclService, checkAcl function

[...]

if (!$this->acl ->isAllowed($role, $controller, $action)){

   //when my AuthService->hasIdentity() returns false, the the role is guest
   if($role=='guest'){ 
      $controllerClass = get_class($e->getTarget());
         //this prevents nested forwards
         if($controllerClass!='Auth\Controller\Auth2Controller'){
         $e->setResult($e->getTarget()->forward()->dispatch('Auth\Controller\Auth2',array('action'=>'index')));
          }

    }
    else{...}
}

And then in my AuthService, I use this function (called in mysite/login) to redirect authenticated users

//if the login page has ben forwarded
if($e->getRouteMatch()->getParam('controller')!='Auth\Controller\Auth2'
{
$url=$this->Config['webhost'].$e->getRequest()->getRequestUri();
return $e->getTarget()->redirect()->toUrl($url);
}
//if the request comes directly from a login/register attempt
else{return $e->getTarget()->redirect()->toRoute('user_home');}

What do you think about it? It makes sense?

Do you know a better approach?


Solution

  • to answer my own question:

    No, it makes no sense. That's because

    Forwarding is a pattern to dispatch a controller when another controller has already been dispatched (Forward to another controller/action from module.php)

    Therefore, the acl check is partially useless because anything placed into a protected page will be executed before the forwarding. This means that if I have a user page (controller) designed for authenticated users only and I call a function like:

    $name=$this->getUserName($userId);
    

    I will get an error message because the var $userId is supposed to be setted during the authentication process. And so on...


    Anyway,I found a better solution with a completely different behaviour but the same result (obviously without the issue described above).

    However, since my original question was very specific,for the sake of clarity I would like to explain better the prefixed goal:

    I got an acl script that runs before dispathcing and redirects the user to a login form if they're not authenticated (role=guest).

    When users send their credential via the login form and they get authenticated, I want to perform a dynamic reidirect depending on how they reached the form.

    The dynamic redirect should follow theese conditions:

    1. When users reach the form via www.site.com/login, once authenticated they should be redirected to www.site.com/user_home

    2. When users reach the form via www.site.com/controller/action/param?var=xxx&var2=yyy[...], once authenticated they should be redirected to the same url without loosing any parameter.

    3. Once users are authenticated, they shoud not be able to reach the page www.site.com/login (because is useless). This means users looking for this page shoud be redirected elsewhere.

    But I also started from the assumption that "redirect is bad":

    1. If not reached directly, there should not be any trace of www.site.com/login into the browser history. This because the browser's back button could screw up anything said in point 2 (conflict). Example:

      • I reach the page site.com/user/post/2016?tag=foo but I'm not logged in or session has expired
      • Then I get redirected to www.site.com/login where I fill up the authentication form and submit it
      • Once authenticated, I get redirected again to site.com/user/post/2016?tag=foo
      • BUT, if I go back via the browser's button, then the origin of my request is www.site.com/login and, as defined in point 1 and 3, I'll be redirected to www.site.com/user_home loosing the ability to return to site.com/user/post/2016?tag=foo (if not via History). (And this is the reason why I was trying to work with the forward plugin)

    The solution I found to achieve this goal is using the redirect plugin in addition to a do-it-yourself forwarding process which works with the MVC's Routemach.

    I started coding following this samsonasik's tutorial: https://samsonasik.wordpress.com/2013/05/29/zend-framework-2-working-with-authenticationservice-and-db-session-save-handler/

    The main difference from samsonasik's code is that I added a service called authService which handles authentication-related functions separately. This just to make the code more readable/reusable since I need some custom logic (related to my application design) to be executed on authentication.

    Here the main sections of my structure:

    Application
    Auth
        ->service
                 ->authService
                 ->aclService
    Guest
         ->Controller
                     ->guestHomeController
                                          ->loginAction
                                          ->singUpAction
                                          ->...
                     ->...
    User
        ->Controller
                    ->userHomeController
                    ->...
    

    Anything inside the guest module is supposed to be accessible to public (role=guest) but not to already authenticated users (according to point 3 above).

    And finally, the code:

    path: Auth/Module.php

    namespace Auth;
    
    use Zend\Mvc\MvcEvent;
    
    class Module
    {
    
      public function onBootstrap(MvcEvent $e)
      {
        $app = $e->getApplication();
        $sm  = $app->getServiceManager();
    
        $acl = $sm->get('aclService');
    
        $acl->initAcl();
        $e -> getApplication() -> getEventManager() -> attach(MvcEvent::EVENT_ROUTE, array($acl, 'checkAcl'));
      }
    
      [...]
    }
    

    path: Auth/src/Auth/Service/AclService.php

    namespace Auth\Service;
    
    use Zend\Permissions\Acl\Acl;
    use Zend\Permissions\Acl\Role\GenericRole as Role;
    use Zend\Permissions\Acl\Resource\GenericResource as Resource;
    use Zend\Mvc\MvcEvent;
    
    
    class AclService  {
    
    protected $authService;
    protected $config;
    protected $acl;
    protected $role;
    
    
        public function __construct($authService,$config)
        {
            if(!$this->acl){$this->acl=new Acl();}
            $this->authService = $authService;
            $this->config = $config;
        }
    
    
        public function initAcl()
        {
           $this->acl->addRole(new Role('guest'));
           [...]
    
           $this->acl->addResource(new Resource('Guest\Controller\GuestHome'));
           [...]
    
           $this->acl->allow('role', 'controller','action');
           [...]
        }
    
    
       public function checkAcl(MvcEvent $e)
        {
          $controller=$e -> getRouteMatch()->getParam('controller');
          $action=$e -> getRouteMatch()->getParam('action');
          $role=$this->getRole();
    
          if (!$this->acl ->isAllowed($role, $controller, $action)){
    
            //if user isn't authenticated...
            if($role=='guest'){
              //set current route controller and action (this doesn't modify the url, it's non a redirect)
              $e -> getRouteMatch()->setParam('controller', 'Guest\Controller\GuestHome');
              $e -> getRouteMatch()->setParam('action', 'login');
            }//if
    
            //if user is authenticated but has not the permission...
            else{
              $response = $e -> getResponse();
              $response -> getHeaders() -> addHeaderLine('Location', $e -> getRequest() -> getBaseUrl() . '/404');
              $response -> setStatusCode(404);
            }//else
          }//if
        }
    
       public function getRole()
       {
       if($this->authService->hasIdentity()){
       [...]
       }
       else{$role='guest';}
       return $role;
       }
    
       [...]
    
    }
    

    path: Guest/src/Guest/Controller/GuestHomeController.php

    namespace Guest\Controller;
    
    use Zend\Mvc\Controller\AbstractActionController;
    use Zend\View\Model\ViewModel;
    use Zend\Form\Factory;
    
    class GuestHomeController extends AbstractActionController
    {
    
        protected $authService;
        protected $config;
        protected $routeName;
    
        public function __construct($authService, $config, $routeMatch)
        {
            $this->authService = $authService;
            $this->config = $config;
            $this->routeName = $routeMatch->getMatchedRouteName();
        }
    
    
        public function loginAction()
        {
    
          $forwardMessage=null;
    
          //When users DOESN'T reach the form via www.site.com/login (they get forwarded), set a message to tell them what's going on
          if($this->routeName!='login'){
            $forwardMessage='You must be logged in to see this page!!!';
          }
    
          //This could probably be moved elsewhere and be triggered (when module is "Guest") on dispatch event with maximum priority (predispatch)
          if ($this->authService->hasIdentity()) {
            $this->dynamicRedirect();
          }
    
          $factory = new Factory();
          $form = $factory->createForm($this->config['LoginForm']);
    
          $request=$this->getRequest();
    
          if ($request->isPost()) {
    
            $form->setData($request->getPost());
    
            if ($form->isValid()) {
    
              $dataform = $form->getData();
              $result=$this->authService->authenticate($dataform);
    
              //result=status code
              if($result===1){$this->dynamicRedirect();}
              else{...}
            }//$form->isValid()
          }//$request->isPost()
    
    
          $viewModel = new ViewModel();
          $viewModel->setVariable('form', $form);
          $viewModel->setVariable('error', $forwardMessage);
    
          return $viewModel;
        }
    
    
        public function dynamicRedirect()
        {
          //if the form has been forwarded
          if($this->routeName!='login'){
            $url=$this->config['webhost'].$this->getRequest()->getRequestUri();
            return $this->redirect()->toUrl($url);
          }
          else{return $this->redirect()->toRoute('userHome');}//else
        }
    
        [...]
    }
    

    I will update this post if I find some other issues but for now it works like a charm.

    P.S.: Hope my English results readable. :-)