Zend Framework 2: Authentication + Acl using EventManager

This is a first try to implement Zend Authentication with Zend Acl under ZF2. I used the EventManager to catch the MVC preDispatch Event so that I can evaluate if the user has or not permission for the requested page.


First I created an extra config for ACL (could be also in module.config.php, but I prefer to have it in a separated file)

  • config/acl.config.php
 array(
        'roles' => array(
            'guest'   => null,
            'member'  => 'guest'
        ),
        'resources' => array(
            'allow' => array(
                'user' => array(
                    'login' => 'guest',
                    'all'   => 'member'
                )
            )
        )
    )
);

Next step is to create the Acl Class where we manage the roles and resources which we have defined in the config

  • src/User/Acl/Acl.php

 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User\Acl;

/**
 * @uses Zend\Acl\Acl
 * @uses Zend\Acl\Role\GenericRole
 * @uses Zend\Acl\Resource\GenericResource
 */
use Zend\Acl\Acl as ZendAcl,
    Zend\Acl\Role\GenericRole as Role,
    Zend\Acl\Resource\GenericResource as Resource;

/**
 * Class to handle Acl
 *
 * This class is for loading ACL defined in a config
 *
 * @category User
 * @package  User_Acl
 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */
class Acl extends ZendAcl {
    /**
     * Default Role
     */
    const DEFAULT_ROLE = 'guest';

    /**
     * Constructor
     *
     * @param array $config
     * @return void
     * @throws \Exception
     */
    public function __construct($config)
    {
        if (!isset($config['acl']['roles']) || !isset($config['acl']['resources'])) {
            throw new \Exception('Invalid ACL Config found');
        }

        $roles = $config['acl']['roles'];
        if (!isset($roles[self::DEFAULT_ROLE])) {
            $roles[self::DEFAULT_ROLE] = '';
        }

        $this->_addRoles($roles)
             ->_addResources($config['acl']['resources']);
    }

    /**
     * Adds Roles to ACL
     *
     * @param array $roles
     * @return User\Acl
     */
    protected function _addRoles($roles)
    {
        foreach ($roles as $name => $parent) {
            if (!$this->hasRole($name)) {
                if (empty($parent)) {
                    $parent = array();
                } else {
                    $parent = explode(',', $parent);
                }

                $this->addRole(new Role($name), $parent);
            }
        }

        return $this;
    }

    /**
     * Adds Resources to ACL
     *
     * @param $resources
     * @return User\Acl
     * @throws \Exception
     */
    protected function _addResources($resources)
    {
        foreach ($resources as $permission => $controllers) {
            foreach ($controllers as $controller => $actions) {
                if ($controller == 'all') {
                    $controller = null;
                } else {
                    if (!$this->hasResource($controller)) {
                        $this->addResource(new Resource($controller));
                    }
                }

                foreach ($actions as $action => $role) {
                    if ($action == 'all') {
                        $action = null;
                    }

                    if ($permission == 'allow') {
                        $this->allow($role, $controller, $action);
                    } elseif ($permission == 'deny') {
                        $this->deny($role, $controller, $action);
                    } else {
                        throw new \Exception('No valid permission defined: ' . $permission);
                    }
                }
            }
        }

        return $this;
    }
}

I decided to use a Controller Plugin (zf1 ActionHelpers) for the Authentication interaction

  • src/User/Controller/Plugin/UserAuthentication.php

 * @copyright  Copyright (c) 2011, Marco Neumann
 * @license    http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User\Controller\Plugin;

/**
 * @uses Zend\Mvc\Controller\Plugin\AbstractPlugin
 * @uses Zend\Authentication\AuthenticationService
 * @uses Zend\Authentication\Adapter\DbTable
 */
use Zend\Mvc\Controller\Plugin\AbstractPlugin,
    Zend\Authentication\AuthenticationService,
    Zend\Authentication\Adapter\DbTable as AuthAdapter;

/**
 * Class for User Authentication
 *
 * Handles Auth Adapter and Auth Service to check Identity
 *
 * @category   User
 * @package    User_Controller
 * @subpackage User_Controller_Plugin
 * @copyright  Copyright (c) 2011, Marco Neumann
 * @license    http://binware.org/license/index/type:new-bsd New BSD License
 */
class UserAuthentication extends AbstractPlugin
{
    /**
     * @var AuthAdapter
     */
    protected $_authAdapter = null;

    /**
     * @var AuthenticationService
     */
    protected $_authService = null;

    /**
     * Check if Identity is present
     *
     * @return bool
     */
    public function hasIdentity()
    {
        return $this->getAuthService()->hasIdentity();
    }

    /**
     * Return current Identity
     *
     * @return mixed|null
     */
    public function getIdentity()
    {
        return $this->getAuthService()->getIdentity();
    }

    /**
     * Sets Auth Adapter
     *
     * @param \Zend\Authentication\Adapter\DbTable $authAdapter
     * @return UserAuthentication
     */
    public function setAuthAdapter(AuthAdapter $authAdapter)
    {
        $this->_authAdapter = $authAdapter;

        return $this;
    }

    /**
     * Returns Auth Adapter
     *
     * @return \Zend\Authentication\Adapter\DbTable
     */
    public function getAuthAdapter()
    {
        if ($this->_authAdapter === null) {
            $this->setAuthAdapter(new AuthAdapter());
        }

        return $this->_authAdapter;
    }

    /**
     * Sets Auth Service
     *
     * @param \Zend\Authentication\AuthenticationService $authService
     * @return UserAuthentication
     */
    public function setAuthService(AuthenticationService $authService)
    {
        $this->_authService = $authService;

        return $this;
    }

    /**
     * Gets Auth Service
     *
     * @return \Zend\Authentication\AuthenticationService
     */
    public function getAuthService()
    {
        if ($this->_authService === null) {
            $this->setAuthService(new AuthenticationService());
        }

        return $this->_authService;
    }

}

Now we can create the Event Handler were we will implement the AuthenticationPlugin (at this point, I think that using a Controller Plugin is not the right way to do it, but for now I will leave it so until I have a better solution…) and the Acl

  • src/Event/Authentication.php

 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User\Event;

/**
 * @uses Zend\Mvc\MvcEvent
 * @uses User\Controller\Plugin\UserAuthentication
 * @uses User\Acl\Acl
 */
use Zend\Mvc\MvcEvent as MvcEvent,
    User\Controller\Plugin\UserAuthentication as AuthPlugin,
    User\Acl\Acl as AclClass;

/**
 * Authentication Event Handler Class
 *
 * This Event Handles Authentication
 *
 * @category  User
 * @package   User_Event
 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */
class Authentication
{
    /**
     * @var AuthPlugin
     */
    protected $_userAuth = null;

    /**
     * @var AclClass
     */
    protected $_aclClass = null;

    /**
     * preDispatch Event Handler
     *
     * @param \Zend\Mvc\MvcEvent $event
     * @throws \Exception
     */
    public function preDispatch(MvcEvent $event)
    {
        [email protected] - Should we really use here and Controller Plugin?
        $userAuth = $this->getUserAuthenticationPlugin();
        $acl = $this->getAclClass();
        $role = AclClass::DEFAULT_ROLE;

        if ($userAuth->hasIdentity()) {
            $user = $userAuth->getIdentity();
            $role = 'member'; [email protected] - Get role from user!
        }


        $routeMatch = $event->getRouteMatch();
        $controller = $routeMatch->getParam('controller');
        $action     = $routeMatch->getParam('action');

        if (!$acl->hasResource($controller)) {
            throw new \Exception('Resource ' . $controller . ' not defined');
        }

        if (!$acl->isAllowed($role, $controller, $action)) {
            $url = $event->getRouter()->assemble(array(), array('name' => 'login'));
            $response = $event->getResponse();
            $response->headers()->addHeaderLine('Location', $url);
            $response->setStatusCode(302);
            $response->sendHeaders();
            exit;
        }
    }

    /**
     * Sets Authentication Plugin
     *
     * @param \User\Controller\Plugin\UserAuthentication $userAuthenticationPlugin
     * @return Authentication
     */
    public function setUserAuthenticationPlugin(AuthPlugin $userAuthenticationPlugin)
    {
        $this->_userAuth = $userAuthenticationPlugin;

        return $this;
    }

    /**
     * Gets Authentication Plugin
     *
     * @return \User\Controller\Plugin\UserAuthentication
     */
    public function getUserAuthenticationPlugin()
    {
        if ($this->_userAuth === null) {
            $this->_userAuth = new AuthPlugin();
        }

        return $this->_userAuth;
    }

    /**
     * Sets ACL Class
     *
     * @param \User\Acl\Acl $aclClass
     * @return Authentication
     */
    public function setAclClass(AclClass $aclClass)
    {
        $this->_aclClass = $aclClass;

        return $this;
    }

    /**
     * Gets ACL Class
     *
     * @return \User\Acl\Acl
     */
    public function getAclClass()
    {
        if ($this->_aclClass === null) {
            $this->_aclClass = new AclClass(array());
        }

        return $this->_aclClass;
    }
}

Now we need to attach the Event Handler to the EventManager, this will be done in the Module Class (haven’t found a better way to do it). I also had to attach an own function and from there trigger the User\Event\Authentication function (not very elegant ;( )

  • Module.php

 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User;

/**
 * @uses Zend\Module\Consumer\AutoloaderProvider
 * @uses Zend\EventManager\StaticEventManager
 */
use Zend\Module\Consumer\AutoloaderProvider,
    Zend\EventManager\StaticEventManager;

/**
 * Module Class
 *
 * Handles Module Initialization
 *
 * @category  User
 * @package   User
 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */
class Module implements AutoloaderProvider
{
    /**
     * Init function
     *
     * @return void
     */
    public function init()
    {
        // Attach Event to EventManager
        $events = StaticEventManager::getInstance();
        $events->attach('Zend\Mvc\Controller\ActionController', 'dispatch', array($this, 'mvcPreDispatch'), 100); [email protected] - Go directly to User\Event\Authentication
    }

    /**
     * Get Autoloader Configuration
     *
     * @return array
     */
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\ClassMapAutoloader' => array(
                __DIR__ . '/autoload_classmap.php'
            ),
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__
                )
            )
        );
    }

    /**
     * Get Module Configuration
     *
     * @return array
     */
    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }

    /**
     * MVC preDispatch Event
     *
     * @param $event
     * @return mixed
     */
    public function mvcPreDispatch($event) {
        $di = $event->getTarget()->getLocator();
        $auth = $di->get('User\Event\Authentication');

        return $auth->preDispatch($event);
    }
}

Here is the config where we attach all needed classes and load acl.config.php

  • config/module.config.php
 array(
        'instance' => array(
            'alias' => array(
                'user' => 'User\Controller\UserController'
            ),
            'user' => array(
                'parameters' => array(
                    'broker' => 'Zend\Mvc\Controller\PluginBroker'
                )
            ),
            'User\Event\Authentication' => array(
                'parameters' => array(
                    'userAuthenticationPlugin' => 'User\Controller\Plugin\UserAuthentication',
                    'aclClass'                 => 'User\Acl\Acl'
                )
            ),
            'User\Acl\Acl' => array(
                'parameters' => array(
                    'config' => include __DIR__ . '/acl.config.php'
                )
            ),
            'User\Controller\Plugin\UserAuthentication' => array(
                'parameters' => array(
                    'authAdapter' => 'Zend\Authentication\Adapter\DbTable'
                )
            ),
            'Zend\Authentication\Adapter\DbTable' => array(
                'parameters' => array(
                    'zendDb' => 'Zend\Db\Adapter\Mysqli',
                    'tableName' => 'users',
                    'identityColumn' => 'email',
                    'credentialColumn' => 'password',
                    'credentialTreatment' => 'SHA1(CONCAT(?, "secretKey"))'
                )
            ),
            'Zend\Db\Adapter\Mysqli' => array(
                'parameters' => array(
                    'config' => array(
                        'host' => 'localhost',
                        'username' => 'username',
                        'password' => 'password',
                        'dbname' => 'dbname',
                        'charset' => 'utf-8'
                    )
                )
            ),
            'Zend\Mvc\Controller\PluginLoader' => array(
                'parameters' => array(
                    'map' => array(
                        'userAuthentication' => 'User\Controller\Plugin\UserAuthentication'
                    )
                )
            ),
            'Zend\View\PhpRenderer' => array(
                'parameters' => array(
                    'options' => array(
                        'script_paths' => array(
                            'user' => __DIR__ . '/../views'
                        )
                    )
                )
            )
        )
    ),
    'routes' => array(
        'login' => array(
            'type' => 'Zend\Mvc\Router\Http\Literal',
            'options' => array(
                'route'    => '/login',
                'defaults' => array(
                    'controller' => 'user',
                    'action'     => 'login',
                )
            )
        )
    )
);

Well, we are now done, we only need to create the Login Form and the Controller

  • src/Form/Login.php

 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User\Form;

/**
 * @uses Zend\Form\Form
 */
use Zend\Form\Form;

/**
 * Login Form Class
 *
 * Login Form
 *
 * @category  User
 * @package   User_Form
 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */
class Login extends Form
{
    /**
     * Initialize Form
     */
    public function init()
    {
        $this->setMethod('post')
             ->loadDefaultDecorators()
             ->addDecorator('FormErrors');

        $this->addElement(
            'text',
            'username',
            array(
                 'filters' => array(
                     array('StringTrim')
                 ),
                 'validators' => array(
                     'EmailAddress'
                 ),
                 'required' => true,
                 'label'    => 'Email'
            )
        );

        $this->addElement(
            'password',
            'password',
            array(
                 'filters' => array(
                     array('StringTrim')
                 ),
                 'validators' => array(
                     array(
                         'StringLength',
                         true,
                         array(
                             6,
                             999
                         )
                     )
                 ),
                 'required' => true,
                 'label'    => 'Password'
            )
        );

        $this->addElement(
            'hash',
            'csrf',
            array(
                 'ignore' => true,
                 'decorators' => array('ViewHelper')
            )
        );

        $this->addElement(
            'submit',
            'login',
            array(
                 'ignore' => true,
                 'label' => 'Login'
            )
        );

    }
}
  • src/User/Controller/UserController.php

 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */

/**
 * @namespace
 */
namespace User\Controller;

/**
 * @uses Zend\Mvc\Controller\ActionController
 * @uses User\Form\Login
 */
use Zend\Mvc\Controller\ActionController,
    User\Form\Login as LoginForm;

/**
 * User Controller Class
 *
 * User Controller
 *
 * @category  User
 * @package   User_Controller
 * @copyright Copyright (c) 2011, Marco Neumann
 * @license   http://binware.org/license/index/type:new-bsd New BSD License
 */
class UserController extends ActionController
{
    /**
     * Index Action
     */
    public function indexAction()
    {
        [email protected] - Implement indexAction
    }

    /**
     * Login Action
     *
     * @return array
     */
    public function loginAction()
    {
        $form = new LoginForm();
        $request = $this->getRequest();

        if ($request->isPost() && $form->isValid($request->post()->toArray())) {
            $uAuth = $this->getLocator()->get('User\Controller\Plugin\UserAuthentication'); [email protected] - We must use PluginLoader $this->userAuthentication()!!
            $authAdapter = $uAuth->getAuthAdapter();

            $authAdapter->setIdentity($form->getValue('username'));
            $authAdapter->setCredential($form->getValue('password'));

            \Zend\Debug::dump($uAuth->getAuthService()->authenticate($authAdapter));
        }

        return array('loginForm' => $form);
    }

    /**
     * Logout Action
     */
    public function logoutAction()
    {
        $this->getLocator()->get('User\Controller\Plugin\UserAuthentication')->clearIdentity(); [email protected] - We must use PluginLoader $this->userAuthentication()!!
    }
}

I know this is not perfect, but this implementation works. For an initial implementation I think it’s ok.

32 Comments to “Zend Framework 2: Authentication + Acl using EventManager”