Zend Books w/ Authentication
— print (last updated: Sep 17, 2009) print

Select font size:
You can either install the project from existing sources or create it from scratch. If use the sources, download the ZendBooksAuth.zip archive, extract it into the desired location and modify to suit your installation:
public/.htaccess     reset RewriteBase if necessary
public/index.php     replace '/usr/local/share/ZendLibrary' if necessary,
This modification of the Books application introduces some new features: The model portion uses the Books and Users class representing the books and users tables, respectively.

In particular, this sample project assumes you have set up Dojo according to the scheme described in the Php AJAX + Dojo document.

Database Setup

This application assumes the common MySQL database setup for testing:
host: localhost
dbname: test
username: guest
password: <empty>
The test database holds the books table with the usual content. Now however, there is an additional admins table used for authentication. The table is very simple:
CREATE TABLE admins (
  id int NOT NULL auto_increment,
  username VARCHAR(50) NOT NULL,
  password VARCHAR(50) NOT NULL,
  PRIMARY KEY(id),
  UNIQUE(username)
);
INSERT INTO admins (username,password) VALUES ('admin', SHA1('foobar'));
There is only one admin. In order to setup the tables, change to the ZendBooksAuth/db directory and run
mysql -u guest test < tables.sql
Check out the contents of the user table by running:
mysql -u guest test
mysql> select * from admins;

Authentication

Zend authentication means that the client established an identity An object of type Zend_Auth is employed to do the authentication test. It is created by the statement:
$auth = Zend_Auth::getInstance();
An authenticated access is one which returns true for the hasIdentity() expression.
$auth->hasIdentity()
This identity is, by default, held as session-based information and so is valid throughout the browser session unless negated. In order to better control the identity for, say, different classes of users, it is useful to specify the the storage of the identity. This is done by:
$authname = "admins";
$auth = Zend_Auth::getInstance();
$auth->setStorage(new Zend_Auth_Storage_Session($authname));
Zend provides other storage mechanisms like database storage, but we'll use session storage. The $authname parameter makes the "session space" a dedicated space separate from others. We use the name "admins" to indicate our intention of authenticating admin users, but the name could be anything.

The identity is negated very simply using the clearIdentity function:
$auth = Zend_Auth::getInstance();
$auth->setStorage(new Zend_Auth_Storage_Session($authname));
$auth->clearIdentity();

Aquiring an identity

Continuing on, the way an identity is acquired is by authenticating against some authentication adapter. In our case we use this code
$authAdapter = My_Helper::getAuthAdapter($username,$password,"admins");
$result = $auth->authenticate($authAdapter);
where the function getAuthAdapter which is defined in a "helper" class, My_Helper.php. The class My_Helper.php corresponds, according to Zend Framework logic, to the actual file situated in the library folder:

library/My/Helper.php
<?php class My_Helper { public static function getAuthAdapter($username, $password, $table) { $db = Zend_Registry::get('db'); $authAdapter = new Zend_Auth_Adapter_DbTable($db); $authAdapter->setTableName($table); $authAdapter->setIdentityColumn('username'); $authAdapter->setCredentialColumn('password'); $authAdapter->setCredentialTreatment('SHA1(?)'); $authAdapter->setIdentity($username); $authAdapter->setCredential($password); return $authAdapter; } public function getAuth($authname) { $auth = Zend_Auth::getInstance(); $auth->setStorage(new Zend_Auth_Storage_Session($authname)); return $auth; } } ?>
Zend auto-loads this class by virtue of the statement we make in the front controller (public/index.php):
$autoloader->registerNamespace("My_");
In this case, the "admins" parameter refers to the admins table in the database. The username and password information are provide by a login form. Together the three parameters are used to create the $authAdapter object.
$authAdapter = My_Helper::getAuthAdapter($username,$password,"admins");
Finally, $authAdapter is used in the authenticate method:
$result = $auth->authenticate($authAdapter);
A successful authentication will make the following true:
$result->isValid()
and the identity is established, meaning that all subsequent calls to
$auth->hasIdentity()
will return true.

Retrieving identity information

The function to use to obtain identity information is:
$auth->getIdentity()
Assuming only what we have done in the steps above, the value of this will be the username for a successful authentication. It is often useful to have other information available as well, such as the id of the authenticated user. In order to achieve this modification, we must do the following exta steps after successful validation:
$data = $authAdapter->getResultRowObject(null, 'password');
$auth->getStorage()->write($data);
The getResultRowObject function, used in this way, stores into the identity the entire table row from the MySQL admins table. The password field is null'd out for additional security. Having done this, we can obtain both the username and id of the authenticated user as member data:
$auth->getIdentity()->username
$auth->getIdentity()->id

Authentication in IndexController

The essence of the authentication used for Create/Update/Delete invocations is to first check if the client has an identity through the helper function:
private function isAuthenticated() {
  $auth = My_Helper::getAuth($this->authname);
  return $auth->hasIdentity();
}
The class variable $this->authname is "admins". Every action which must be protected starts off with this line:
if (! $this->isAuthenticated()) { ...
If authentication fails, then the login action is activated:
$this->_forward('login');
The function used is this:
public function loginAction() {
  $this->view->navMenu = array($this->listing);
  $this->view->authname = $this->authname;
  $this->_forward('login','validate');
}
This action sets up the passing of authname and defers to the login action in the validate controller: It's the job of the validate controller to generate a login form, establish the identity and reload the initial activation so that we can take up "where we left off", now authenticated.

ValidateController and AJAX usage

The full code of ValidateController can be seen here: ( click to show )

The validate/login action is this:
public function loginAction() {
  $this->view->headScript()->appendFile( "/dojolib/dojo/dojo.js" );
  $baseUrl = $this->getRequest()->getBaseUrl();
  $this->view->headScript()->appendFile( $baseUrl . "/js/validate.js" );
  $this->view->baseUrl = $baseUrl;
}
and it invokes this form:

application/views/scripts/validate/login.phtml
<form id="loginform" autocomplete="off" > <input type="hidden" name="authname" value="<?= $this->authname ?>" /> <table cellpadding="5px"> <tr> <td colspan="2"><h2>Protected Access</h2></td> </tr> <tr> <td>Username</td> <td><input type="text" name="username" /></td> </tr> <tr> <td>Password</td> <td><input type="password" name="password" /></td> </tr> <tr> <td></td> <td> <input type="submit" value="Login" onclick="validate('<?=$this->baseUrl?>');return false;" /> </td> </tr> </table> </form>
Submitting the form invokes the JavaScript validate function:

public/js/validate.js
function validate(baseUrl) { dojo.xhrPost( { url: baseUrl + "/validate/check", form: 'loginform', load: function(response){ if (!response) location.reload() else { alert(response) dojo.byId('loginform').password.value = "" } }, error: function(response) { alert("error" + response) } } ) }
The validate function calls the validate/check action through an AJAX call. The check action does all the hard work:
public function checkAction() {
  $this->_helper->layout->disableLayout();
  $this->_helper->viewRenderer->setNoRender();
  
  $req = $this->getRequest();
   
  $username = $req->getParam('username');
  if (!$username) {
    $this->_response->setBody("Missing Username");
    return;
  }
  
  $password = $req->getParam('password');
  $authname = $req->getParam('authname');

  $auth = My_Helper::getAuth($authname);

  $authAdapter = My_Helper::getAuthAdapter($username,$password,$authname);
  $result = $auth->authenticate($authAdapter);

  if (!$result->isValid()) {
    $this->_response->setBody("Failed Access");
  } else {
    $data = $authAdapter->getResultRowObject(null, 'password');
    $auth->getStorage()->write($data);
  }
}
There are several important points:
  1. The correct $authname value (admins in this case) is passed to the check action as follows:
  2. We want to completely control the output sent through the AJAX call and so we completely "turn off" the default Zend behavior with these statements:
    $this->_helper->layout->disableLayout();
    $this->_helper->viewRenderer->setNoRender();
    
  3. A successful validation will send no reponse data whatsoever and an unsuccessful one will send information which can be posted for the user. This, then makes sense of the Dojo AJAX call's load response.

Index Controller/View code

Regarding the index controller and its actions, the scripts show.phtml, showcart.phtml, create.phtml, and modify.phtml are identical to those in Zend Books. The changes to the remaining files are discussed below.

One other novelty is the use of the automatically called Zend function postDispatch. This works like the opposite of init — it is called after each action before activating the view script.

IndexController

The full code of IndexController can be seen here: ( click to show )

Here is a detailed outline of the modifications/additions to IndexController in Zend Books. Added member functions and data appear bolded.
class IndexController extends Zend_Controller_Action {

  private $authname = "admins";

  public function init() { /* no change */ }

  public function indexAction() {
    // the cart session space name differs
    // the dojo file is included
    $this->view->headScript()->appendFile( "/dojolib/dojo/dojo.js" );
  }

  public function showAction() { /* no change */ }

  public function showcartAction() { /* no change */ }
  public function addcartAction() { /* no change */ }
  public function clearcartAction() { /* no change */ }

  public function isAuthenticated() {
     $auth = My_Helper::getAuth($this->authname);
    return $auth->hasIdentity();
  }
  
  public function loginAction() {
    $this->view->navMenu = array($this->listing);
    $this->view->authname = $this->authname;  // send to view
    $this->_forward('login','validate');
  }

  public function logoutAction() {
    $auth = My_Helper::getAuth($this->authname);
    $auth->clearIdentity();
    $this->_helper->redirector("index");
  }

  // append "Logout" choice to menu once "logged in"
  public function postDispatch() {
    if ($this->isAuthenticated()) {
      $auth = My_Helper::getAuth($this->authname);
      $username = $auth->getIdentity()->username;
      $logout = array("label"=>"Logout ($username)",
                      "url" => array("action" => "logout"));
      $this->view->navMenu =
        array_merge($this->view->navMenu,array($logout));
    }
  }

  public function modifyAction() {
    if (! $this->isAuthenticated()) {
      $this->view->navMenu = array($this->listing);
      $this->_forward('login','validate');
      return;
    }
    // the rest is the same as before
   }
   
  public function updateAction() {
    if (! $this->isAuthenticated()) {
      return;
    }
    // the rest is the same as before
  }

  private function check($book) { /* same as before */ }

  public function createAction() {
    if (! $this->isAuthenticated()) {
      $this->view->navMenu = array($this->listing);
      $this->_forward('login','validate');
      return;
    }
    $this->view->pageTitle = "Create";
  }

  public function addAction() {
    if (! $this->isAuthenticated()) {
      return;
    }
    // the rest is the same as before
  }
 
  public function checkauthAction() {         // AJAX-based action
    $this->_helper->layout->disableLayout();
    $this->_helper->viewRenderer->setNoRender();
    if (! $this->isAuthenticated()) {
       $this->_response->setBody("Failed Access");
    }
  }
 
  public function deleteAction() {     // this is new
    if (! $this->isAuthenticated()) {
      $this->view->navMenu = array($this->listing);
      $this->_forward('login');
      return;
    }
    $this->_helper->redirector('index');
  }

  public function delAction() {
    if (! $this->isAuthenticated()) {
      return;
    }
    // the rest is the same as before
  }
}
The JavaScript del function has also been modified significantly from the previous ZendBooks application. It now makes an AJAX call to the /index/checkauth. If the authentication fails, it calls /index/delete which is simply a vehicle to call /index/login to authenticate and return to /index/index upon success.

Once authentication has been completed, delete will act as it did before after successfully completing the /index/checkauth test.

public/js/del.js
function del(theForm,baseUrl) { dojo.xhrGet( { url: baseUrl + "/index/checkauth", load: function(response){ if (response) { location.replace(baseUrl+"/index/delete") } else { if (confirm("Are you really, really sure ?")) { theForm.submit(); } } }, error: function(response) { alert("error" + response) } } ) }
Observe how the actions addAction, updateAction, and delAction which actually do the changes are protected even though they are not called directly. This is a security measure done in order to prevent them from being activated "outside" the intended activation procedure.

index view

Regarding the view functions, the only real change is to the index/index activation script in that the del function is called differently:
<button onclick="del(this.parentNode, '<?=$this->baseUrl?>');return false;"
        >delete</button>
The full code of index/index.phtml can be seen here: ( click to show )


© Robert M. Kline