Scalable Authorization with Zend Framework

The Zend Framework offers a highly evolved and complex control flow model. The ZF provides three distinct ways to insert code – Front Plugins, Action Helpers and Controllers. The interplay between these is best expressed by the  diagram at http://surlandia.com/wp-content/uploads/2008/11/zf-dispatch-lifecycle-bw.jpg

Authorization to a page(controller/action) is to be checked on EVERY page request.

The main mechanism of implementing Authorization in the ZF is using Zend_ACL. If you are not careful with implementing this, you could easily end up loading the ACL object with EVERY action and controller in the application. We do NOT want to spend the time and memory loading the ACL with resources that will never be required!

A couple of months ago, I had written about how the ZF authorization could be automated using database tables. While this approach tries to alleviate some of the problems associated with dynamic ACL loading using a session variable, it is still wasteful of resources and is not advisable for large apps.

The following technique is, in my opinion, a smarter approach to handling ACL’s and authorization (Credit goes to Rob Allen, author of Zend Framework in Action). The key to using this method is to understand the correct place to implement ACL checking and rule creation.

image

Here is a simplified flowchart of what we plan to do:

image

Create a custom ACL class and load all the roles (along with inheritance information). For establishing the inheritance hierarchy, we can add the following lines to the config.ini

acl.roles.guest=null
acl.roles.member=guest
acl.roles.admin=member

the member role inherits from guest and admin role inherits from member.. Simple!

<?php
class CustomACL extends Zend_Acl
{
public function __construct()
{
$config = Zend_Registry::get(‘config’);
$roles = $config->acl->roles;
$this->addRoles($roles);
}
protected function addRoles($roles)
{
foreach ($roles as $child=>$parents)
{
if (!$this->hasRole($child))
{
if (empty($parents))
$parents=null;
else
$parents = explode(‘,’,$parents);
$this->addRole(new Zend_Acl_Role($child),$parents);
}
}
}
}
?>

Next, we code the Action Controller Plugin:

<?php
class AuthHelper extends Zend_Controller_Action_Helper_Abstract
{
protected $auth;
protected $acl;
protected $controllerName;
protected $actionName;
protected $defaultRole=’guest’;
public function __construct(Zend_View_Interface $view=null, array $options)
{
$this->auth=Zend_Auth::getInstance();
$this->acl = $options[‘acl’];
}
public function preDispatch()
{
$this->controllerName = $this->getActionController()->getRequest()->getControllerName();
$this->actionName = $this->getActionController()->getRequest()->getActionName();
if (!$this->acl->has($this->controllerName))
{
$this->acl->add(new Zend_Acl_Resource($this->controllerName));
}
if ($this->auth->hasIdentity())
{
$session_role = new Zend_Session_Namespace(‘role’);
$role=$session_role->role;
}
else
$role=$this->defaultRole;
//redirect to login page if not authorized!
if (!$this->acl->isAllowed($role,$this->controllerName, $this->actionName))
{
$this->getActionController()->getRequest()->setControllerName(‘auth’);
$this->getActionController()->getRequest()->setActionName(‘login’);
$this->getActionController()->getRequest()->isDispatched()=false; //this line is important!

}
}
public function allow($roles=null, $actions=null)
{
$resource = $this->controllerName;
$this->acl->allow($roles,$resource, $actions);
return $this;
}
public function deny($roles=null, $actions=null)
{
$resource = $this->controllerName;
$this->acl->deny($roles,$resource, $actions);
return $this;
}
}
?>

PLEASE NOTE the $this->getActionController()->getRequest()->isDispatched()=false directive immediately after setting the new controller and action. If this is not included, it would execute the code within the original action anyway .. although the results will be discarded from buffer and never rendered on screen (You can put in a log message to verify this!). Adding this directive will skip the code in the action function. This will make more sense after you understand the detailed dispatch control flow within the Zend Framework.

dispatch

The index.php bootstrapper is modified accordingly to load up the front controller plugin above:

$acl = new CustomACL();
$authHelper = new AuthHelper(null,array(‘acl’=>$acl));
Zend_Controller_Action_HelperBroker::addHelper($authHelper);

Here’s what my BaseController.php and AuthController.php look like:

<?php
class BaseController extends Zend_Controller_Action
{
protected $db;
protected $auth;
public function preDispatch()
{
$this->db = Zend_Db_Table::getDefaultAdapter();
$this->view->baseUrl = Zend_Controller_Front::getInstance()->getBaseUrl();
$this->auth=Zend_Auth::getInstance();
}
}
?>

<?php
class AuthController extends BaseController
{
public function init()
{
$this->_helper->AuthHelper->allow(null);
}
public function indexAction()
{
$this->_forward(‘login’);
}
public function loginAction()
{
if ($this->auth->hasIdentity())
$this->_redirect(‘index’);

$config = Zend_Registry::get(‘config’);
$errors = array ();
$request = $this->getRequest();

if ($request->isPost())
{
$redirect = $request->getPost(‘redirect’);
$username = $request->getPost(‘username’);
$password = $request->getPost(‘password’);
if (strlen($username) == 0)
$errors[‘username’] = ‘Username is a required
field’;
if (strlen($password) == 0)
$errors[‘password’] = ‘password is a required field’;
if (count($errors) == 0)
{
//data is valid.. do authenticate
$authAdapter = $this->getAuthAdapter($username, $password);
$result = $this->auth->authenticate($authAdapter);

if ($result->isValid())
{
//success.. store data in session..
//and redirect
$this->getRole($username);
$this->_redirect($redirect);

}
else
{
//ldap auth failed
$errors[‘auth’] = “LDAP authentication failed.. please check username and password”;
}
}
}

$this->view->errors = $errors;
$this->view->redirect = $redirect;
//throw the login page again!
}
public function logoutAction()
{
$this->auth->clearIdentity();
$this->_redirect(‘/’);
}
protected function getAuthAdapter($username, $password)
{
$config = Zend_Registry::get(‘config’);
//get the ldap adapter and return to caller.
$authAdapter = new Zend_Auth_Adapter_Ldap($config->ldap->toArray(), $username, $password);
//print_r($username.$password);
return $authAdapter;
}
protected function getRole($username)
{
$db = Zend_Db_Table::getDefaultAdapter();
$result = $db->fetchOne(“select role from users where userid=?”,$username);
$session_role = new Zend_Session_Namespace(‘role’);
$session_role->role=$result;
}
}
?>

The loginAction() attempts an LDAP authentication. If successfully authenticated, a call is made to getRole(). This function looks up the database table named ‘users’ and determines the ‘role’ to which the user belongs. This role is stored in a session variable – Note that this is accessed in the preDispatch() method of our action controller plugin.

Also note the init() function, it basically gives everyone access to the Auth controller.

The login.phtml file code follows:

<?php
$errors = $this->errors;
if (strlen($this->redirect)==0)
$redirect = str_replace(“/internal/inventory/”, “/”, $_SERVER[‘REQUEST_URI’]);
else
$redirect = $this->redirect;
?>
<h2>Sorry! You are not authorized to use the section</h2>
<h1>Please login here</h1>

<div class=”error”> <?php if (isset($errors[‘auth’])) echo $errors[‘auth’] ?></div>
<form method=”post” action=”<?php echo $this->baseUrl?>/auth/login”>
<input type=”hidden” name=”redirect” value=”<?php echo  $redirect?>” />
<div>
<label>
Username
</label>
<input type=”text” name=”username” value=””/>
<div class=”error”><?php if (isset($errors[‘username’])) echo $errors[‘username’]?></div>
</div>
<div>
<label>
Password
</label>
<input type=”password” name=”password” value=”” />
<div class=”error”><?php if (isset($errors[‘password’])) echo $errors[‘password’]?></div>
</div>
<div>
<input type=”submit” name=”login” value=”Login”/>
</div>
</form>

Note the use of the ‘Redirect’ hidden variable… That is used to redirect users to the original page after due authentication.

Thats it! Now, you just have to add init() functions on your controllers to specify ACL rules.. The rest is handled at runtime!

A more involved init() function would look like:

class InternalController extends BaseController {

function init()
{
$guestActions = array(‘index’);
$this->_helper->AuthHelper->allow(‘guest’, $guestActions);
$adminActions = array(‘add’, ‘edit’);
$this->_helper->AuthHelper->allow(‘admin’, $adminActions);
}

……

……

}

What the above code does is grant the ‘guest’ role access to /internal/index and demand the ‘admin’ role for urls ‘/internal/add’ and ‘/internal/edit’.

Thats it! Please feel free to drop your comments and questions.

Demo – Zend Auth and Zend ACL (Part 2)

So far, we have a working ZF application that displays the login page. Note that the form action points to /auth/identify. So, control is transferred to the identify Action in the AuthController class once the submit button is clicked.

public function identifyAction()
    {
        $errors=array();
        $request = $this->getRequest();
        if ($request->isPost())
        {
            $username = $request->getPost(‘username’);
            $password = $request->getPost(‘password’);
                $authAdapter = $this->_getAuthAdapter($username,$password);
                $auth=Zend_Auth::getInstance();
                $result = $auth->authenticate($authAdapter);
                if ($result->isValid())
                {
                    //success.. store row for future use..
                    $data = $authAdapter->getResultRowObject();
                    $auth->getStorage()->write($data);
                    $this->_redirect(‘/’);
                }
                else
                {
                    echo "Login Failed.. Please retry";
                }
        }
        $this->_redirect(‘/auth/login’);
    }

Note the call to $this->_getAuthAdapter($username,$password). This is where the Zend Auth Adapter is initialized and put to use. This function takes the entered username and password, checks for invalid input, then uses the Zend_Auth library to verify the user and transfer control appropriately

private function _getAuthAdapter($username, $password)
    {
        $dbAdapter = self::$db;
        $authAdapter = new Zend_Auth_Adapter_DbTable($dbAdapter,’users’,’username’,’password’);
        $authAdapter->setIdentity($username);
        $authAdapter->setCredential($password);
        return $authAdapter;
    }

The function getResultRowObject() returns the entire row corresponding to the username that is authenticated in the users table. This is stored in the session for later retrieval (Note that the user_type column is also stored along with the user and is used subsequently for authorization purposes).

Go ahead and fire up the login action and ensure that you see the whole process (Login form to Index page on successful auth and login form to login form on unsuccessful auth)

There is actually a small problem with the above form.. The “Login Failed..” message is never really displayed on the screen. This is because of the redirect command. It clears the output buffer before transferring control. We will now fix this with appropriate error messages:

Modify the login.phtml form to include error messages

<h1>Login</h1>
<div id="error"> <?php if (isset($this->errors[‘auth’])) echo $this->errors[‘auth’]?> </div>
<p>Please log in here</p>
<form method="post" action='<?php echo $this->baseUrl?>/auth/identify’>
<div>
<label>Username</label>
<input type="text" name="username" vale=""/>
<div id="error"> <?php if (isset($this->errors[‘username’])) echo $this->errors[‘username’]?> </div>
</div>
<div>
<label>Password</label>
<input type="password" name="password" value="" />
</div>
<div>
<input type="submit" name="login" value="Login" />
</div>
</form>

Also modify the identify action to store the appropriate error messages and finally store it in the view:

public function identifyAction()
    {
        $errors=array();
        $request = $this->getRequest();
        if ($request->isPost())
        {
            $username = $request->getPost(‘username’);
            $password = $request->getPost(‘password’);
            if (strlen($username)==0)
               $errors[‘username’]=’Username is required’;
            if (count($errors)==0)
            {
                //no errors with form.. process
                $authAdapter = $this->_getAuthAdapter($username,$password);
                $auth=Zend_Auth::getInstance();
                $result = $auth->authenticate($authAdapter);
                if ($result->isValid())
                {
                    //success.. store row
                    $data = $authAdapter->getResultRowObject();
                    $auth->getStorage()->write($data);
                    $this->_redirect(‘/’);
                }
                else
                {
                    $errors[‘auth’]= "Login Failed.. Please retry";
                }
            }
        }
        $this->view->errors=$errors;
        $this->forward(‘login’);
    }

There.. now you have a fully functional authentication framework. The next part will focus on Ajax-ing the error messages.

Click here to go over to the next part in this series…

Demo – Zend Auth and Zend ACL (Part 1)

This is a multipart series that uses ZF to perform robust web based authentication and authorization. In the latter parts, AJAX is injected into the framework.

Create the Zend framework directory structure

image

The usual ‘controllers’, ‘models’ and ‘views’ directories hold the core mvc code. The include is setup to hold libraries that we will create that will be shared across the app (and which do not fit the paradigm of m, v or c).

Only the ‘web’ folder will be exposed via the webserver as a virtual directory. This is for added security.. You do not want any of your business intelligence files accessible via the web. We will let the ZF do all the heavy lifting for us.

Go ahead and setup the virtual directory to point to MyZF\web

image

After this is done, the actual application can be accessed at http://localhost/MyZF

One other preparatory step is to make sure that ALL requests to our application get routed through our bootstrap file – index.php

If you are using IIS, add the required directives to the web.config file in the web folder (.htaccess if you are using apache).

Creating the bootstrapper : The next item is to create an index.php in the web folder that will accept ALL incoming requests, initialize the Zend Framework and route accordingly.

We will start off with a basic configuration and refactor as required:

<?php
require_once ‘Zend/Loader.php’;
Zend_Loader::registerAutoload();

//setup the layout
Zend_Layout::startMvc(array(‘layoutpath’=>’../views/layouts’));
try
{
    //Initialize the front controller
    $controller = Zend_Controller_Front::getInstance();
    $controller->setControllerDirectory(‘../controllers’);
    $controller->throwExceptions(true);

//go
    $controller->dispatch();
}
catch (Exception $ex)
{
    header(‘Content-Type: text/html; charset=utf-8’);
    echo ‘An unexpected error occurred’;
    echo ‘<h2>’.$ex->getMessage().'</h2>’;
}
?>

Note that in order to use the try/catch block, the throwExceptions(true) has to be set. We will remove that in the future once our custom error controller is ready.

I consider the Zend_Layout to be one of the really cool features of the Zend framework. It forces a uniform look and feel throughout your application. And, it is real easy to setup.

Database : We will create and use a table named ‘users’ to perform standard database authentication:

image

The username and password entered on a login form by the user will be checked against the username and password fields in this database. Note that in a production environment you will probably not store the actual password but a hash of it.

Next, create a config.ini settings file to simplify database access from ZF.

[general]
db.adapter=PDO_MYSQL
db.config.host=localhost
db.config.username=root
db.config.password=password
db.config.dbname=myzf

The modified index.php with the database initialization settings changes to:

<?php
require_once ‘Zend/Loader.php’;
Zend_Loader::registerAutoload();

$config = new Zend_Config_Ini(‘../config.ini’,’general’);
$db = Zend_Db::factory($config->db->adapter,$config->db->config->toArray());
Zend_Db_Table::setDefaultAdapter($db);
//setup the layout
Zend_Layout::startMvc(array(‘layoutpath’=>’../views/layouts’));
try
{
    //Initialize the front controller
    $controller = Zend_Controller_Front::getInstance();
    $controller->setControllerDirectory(‘../controllers’);
    $controller->throwExceptions(true);

//go
    $controller->dispatch();
}
catch (Exception $ex)
{
    header(‘Content-Type: text/html; charset=utf-8’);
    echo ‘An unexpected error occurred’;
}
?>

Creating controllers

Controller classes are the building blocks of a ZF application.

Create a base controller which abstracts the common methods for use in all controllers (All our controllers will inherit form BaseController instead of Zend_Controller_Action):

<?php
class BaseController extends Zend_Controller_Action
{
    protected static $db=null;
    protected static $baseUrl=null;
    protected static $authenticated=null;
    function init()
    {
        //store the db connection info for use throughout app..
        if (self::$baseUrl===null)
        {
            self::$db = Zend_Db_Table::getDefaultAdapter()

     }
        if (self::$baseUrl===null)
        {
            self::$baseUrl=Zend_Controller_Front::getInstance()->getBaseUrl();
        }
        $this->view->baseUrl = self::$baseUrl;
      }
}
?>

This class provides a convenient handle to the database, baseurl and authenticated flag. These may potentially be required on every page. The use of static variables is an optimisation measure.

Let us create code that renders the default page (index). The index action in the index controller is the code that is executed when the default page is invoked.

<?php
class IndexController extends BaseController
{
    public function indexAction()
    {
        //do nothing.. display the default index.phtml
    }
}
?>

A really simple index.phtml will work for our purposes:

<h1>Welcome to MyZF</h1>

Now, if you point your browser to http://localhost/myzf, you should see the welcome message on the page.

We are now ready to create a separate controller named AuthController that handles all aspects of user authentication. The shell is as shown below:

<?php class AuthController extends BaseController
{
    public function indexAction()
    {
        $this->_forward(‘login’);
    }
    public function loginAction()
    {
    }
    public function logoutAction()
    {
       Zend_Auth::getInstance()->clearIdentity();
       $this->_redirect(‘/’);
    }
    public function identifyAction()
    {
    }
}
?>

The indexAction simiply forwards the request to the loginAction routine (indexAction is an abstract method and MUST be implemented).  The logout action simply uses the Zend_Auth instance to clear the Identity. The regular login form (/views/scripts/auth/login.phtml) with fields for username and password looks like:

<h1>Login</h1>
<p>Please log in here</p>
<form method="post" action='<?php echo $this->baseUrl?>/auth/identify’>
<div>
<label>Username</label>
<input type="text" name="username" vale=""/>
</div>
<div>
<label>Password</label>
<input tyep="password" name="password" value="" />
</div>
<div>
<input type="submit" name="login" value="Login" />
</div>
</form>

Notice the use of $this->baseUrl… This is available to us in the view because of our base controller class!

If you run the application that has been developed so far at

http://localhost/myzf/auth

You should see the login page (embedded neatly inside your general layout page)

Click here to go over to the next part in this series…