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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
<?php return array( 'acl' => 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
<?php /** * File for Acl Class * * @category User * @package User_Acl * @author Marco Neumann <webcoder_at_binware_dot_org> * @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
<?php /** * File for UserAuthentication Class * * @category User * @package User_Controller * @subpackage User_Controller_Plugin * @author Marco Neumann <webcoder_at_binware_dot_org> * @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
<?php /** * File for Event Class * * @category User * @package User_Event * @author Marco Neumann <webcoder_at_binware_dot_org> * @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) { //@todo - 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'; //@todo - 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
<?php /** * File for Module Class * * @category User * @package User * @author Marco Neumann <webcoder_at_binware_dot_org> * @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); //@todo - 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
<?php return array( 'di' => 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
<?php /** * File for Login Form Class * * @category User * @package User_Form * @author Marco Neumann <webcoder_at_binware_dot_org> * @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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
<?php /** * File for User Controller Class * * @category User * @package User_Controller * @author Marco Neumann <webcoder_at_binware_dot_org> * @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() { //@todo - 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'); //@todo - 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(); //@todo - 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.