A free and open-source book on ZF3 for beginners


16.8. Access Filtering

The last thing we implement in the User module is the access filter. The access filter will be used for restricting access to certain web pages to authenticated users only.

The access filter will work as follows:

The access filter is designed to function in one of two modes: restrictive (default) and permissive. In restrictive mode the filter forbids access to any page not listed under the access_filter key.

The access_filter configuration key will be located inside the module.config.php file and will be used by the access filter. It will contain the list of controllers and action names, and for each action it will either allow to anyone to see the page or allow to authenticated users only to see the page. An example structure of the key is presented below:

// The 'access_filter' key is used by the User module to restrict or permit
// access to certain controller actions for unauthenticated visitors.
'access_filter' => [
    'options' => [
        // The access filter can work in 'restrictive' (recommended) or 'permissive'
        // mode. In restrictive mode all controller actions must be explicitly listed 
        // under the 'access_filter' config key, and access is denied to any not listed 
        // action for users not logged in. In permissive mode, if an action is not listed 
        // under the 'access_filter' key, access to it is permitted to anyone (even for 
        // users not logged in. Restrictive mode is more secure and recommended.
        'mode' => 'restrictive'
    ],
    'controllers' => [
        Controller\IndexController::class => [
            // Allow anyone to visit "index" and "about" actions
            ['actions' => ['index', 'about'], 'allow' => '*'],
            // Allow authenticated users to visit "settings" action
            ['actions' => ['settings'], 'allow' => '@']
        ],
    ]
],

Under the access_filter key, we have two subkeys:

The access filter implementation is very simple. It can't, for example, allow access based on username or by user role. However, you can easily modify and extend it as you wish. If you plan to introduce role-based access control (RBAC), refer to the Role-Based Access Control chapter.

16.8.1. Adding Dispatch Event Listener

To implement access filtering, we will use an event listener. You have already become familiar with event listening in the Creating a New Module chapter.

Particularly, we will listen to the Dispatch event. The Dispatch event is triggered after the Route event, when the controller and action are already determined. To implement the listener, we modify the Module.php file of the User module as follows:

<?php
namespace User;

use Zend\Mvc\MvcEvent;
use Zend\Mvc\Controller\AbstractActionController;
use User\Controller\AuthController;
use User\Service\AuthManager;

class Module
{
    /**
     * This method returns the path to module.config.php file.
     */
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }
    
    /**
     * This method is called once the MVC bootstrapping is complete and allows
     * to register event listeners. 
     */
    public function onBootstrap(MvcEvent $event)
    {
        // Get event manager.
        $eventManager = $event->getApplication()->getEventManager();
        $sharedEventManager = $eventManager->getSharedManager();
        // Register the event listener method. 
        $sharedEventManager->attach(AbstractActionController::class, 
                MvcEvent::EVENT_DISPATCH, [$this, 'onDispatch'], 100);
    }
    
    /**
     * Event listener method for the 'Dispatch' event. We listen to the Dispatch
     * event to call the access filter. The access filter allows to determine if
     * the current visitor is allowed to see the page or not. If he/she
     * is not authenticated and is not allowed to see the page, we redirect the user 
     * to the login page.
     */
    public function onDispatch(MvcEvent $event)
    {
        // Get controller and action to which the HTTP request was dispatched.
        $controller = $event->getTarget();
        $controllerName = $event->getRouteMatch()->getParam('controller', null);
        $actionName = $event->getRouteMatch()->getParam('action', null);
        
        // Convert dash-style action name to camel-case.
        $actionName = str_replace('-', '', lcfirst(ucwords($actionName, '-')));
        
        // Get the instance of AuthManager service.
        $authManager = $event->getApplication()->getServiceManager()->get(AuthManager::class);
        
        // Execute the access filter on every controller except AuthController
        // (to avoid infinite redirect).
        if ($controllerName!=AuthController::class && 
            !$authManager->filterAccess($controllerName, $actionName)) {
            
            // Remember the URL of the page the user tried to access. We will
            // redirect the user to that URL after successful login.
            $uri = $event->getApplication()->getRequest()->getUri();
            // Make the URL relative (remove scheme, user info, host name and port)
            // to avoid redirecting to other domain by a malicious user.
            $uri->setScheme(null)
                ->setHost(null)
                ->setPort(null)
                ->setUserInfo(null);
            $redirectUrl = $uri->toString();
            
            // Redirect the user to the "Login" page.
            return $controller->redirect()->toRoute('login', [], 
                    ['query'=>['redirectUrl'=>$redirectUrl]]);
        }
    }
}

16.8.2. Implementing Access Filtering Algorithm

The onDispatch() event listener calls the filterAccess() method of AuthManager service to determine if the page can be seen or not. The code of the filterAccess() method is presented below:

/**
 * This is a simple access control filter. It allows vistors to visit certain pages only,
 * the rest requiring the user to be authenticated. 
 * 
 * This method uses the 'access_filter' key in the config file and determines
 * whenther the current visitor is allowed to access the given controller action
 * or not. It returns true if allowed; otherwise false.
 */
public function filterAccess($controllerName, $actionName)
{
    // Determine mode - 'restrictive' (default) or 'permissive'. In restrictive
    // mode all controller actions must be explicitly listed under the 'access_filter'
    // config key, and access is denied to any not listed action for unauthenticated users. 
    // In permissive mode, if an action is not listed under the 'access_filter' key, 
    // access to it is permitted to anyone (even for not logged in users.
    // Restrictive mode is more secure and recommended to use.
    $mode = isset($this->config['options']['mode'])?$this->config['options']['mode']:'restrictive';
    if ($mode!='restrictive' && $mode!='permissive')
        throw new \Exception('Invalid access filter mode (expected either restrictive or permissive mode');
    
    if (isset($this->config['controllers'][$controllerName])) {
        $items = $this->config['controllers'][$controllerName];
        foreach ($items as $item) {
            $actionList = $item['actions'];
            $allow = $item['allow'];
            if (is_array($actionList) && in_array($actionName, $actionList) ||
                $actionList=='*') {
                if ($allow=='*')
                    return true; // Anyone is allowed to see the page.
                else if ($allow=='@' && $this->authService->hasIdentity()) {
                    return true; // Only authenticated user is allowed to see the page.
                } else {                    
                    return false; // Access denied.
                }
            }
        }            
    }
    
    // In restrictive mode, we forbid access for authenticated users to any 
    // action not listed under 'access_filter' key (for security reasons).
    if ($mode=='restrictive' && !$this->authService->hasIdentity())
        return false;
    
    // Permit access to this page.
    return true;
}

16.8.3. Testing Access Filter

To test the access filter, try to visit the "http://localhost/users" or "http://localhost/settings" page when you are not logged in. The access filter will direct you to Login page. However, you can easily visit the "http://localhost/about" page - it is open to anyone.


Top