Advancing My First Zend App, Part 2


Now it's time to build an authentication system for our application, so that we have public stuff for browsing while all the add/edit/delete functions are secured.  There are of course a variety of ways to authenticate users and Zend has adapters for several popular ones including DB, the somewhat antiquated HTTP, and LDAP – you can also build your own.  For this app, DB will work fine.  So I need a table to store stuff – nothing too fancy for this.

CREATE TABLE admins (
	username VARCHAR(30),
	password VARCHAR(255)
);

Since this authentication would be for the whole app, not just our one module, I'll make it as a separate module called Authentication. Since this post is gonna have a ton of code, I added comments within each code block to help clear up what things are doing, then I'll discuss any highlights as needed.

The Module.php for this new module has some of our usual fare…but it also incorporates the onBootstrap function and a custom function to handle the log in stuff.

<?php
	namespace Authentication;
	
	use Zend\Mvc\ModuleRouteListener;
	use Zend\Mvc\MvcEvent;
	
	class Module {
		public function onBootstrap(MvcEvent $e) {
			$eventManager = $e->getApplication()->getEventManager();
			$moduleRouteListener = new ModuleRouteListener();
			$moduleRouteListener->attach($eventManager);
			
			// whenever an event is dispatched, call our authorizationCheck function
			$eventManager->attach(MvcEvent::EVENT_DISPATCH, array($this, 'authorizationCheck'), 1);
		}
		
		public function authorizationCheck(MvcEvent $event) {
			// get some variables for easier reference later
			$requestedRoute = $event->getRouteMatch()->getMatchedRouteName();
			$requestedAction = $event->getRouteMatch()->getParam('action');
			
			$authenticationService = $event->getApplication()
				->getServiceManager()
				->get('AuthenticationService');
			
				
			// we only care about authentication on public events.
			if (strpos($requestedAction, 'public') === false) {
				// If they are trying to log, just let them do so, no need to do an endless loop now :-)
				if ($requestedRoute === 'login') {
					return;
				}
				// check authentication, if not, redirect to log in
				elseif ($authenticationService->isAuthenticated() === false) {
					$response = $event->getResponse();
					$response->getHeaders()
						->clearHeaders()
						->addHeaderLine(
							'Location', '/login'
						);
					$response->setStatusCode(302)
						->sendHeaders();
						
					exit;	
				}
				// if we're authenticated, freshen up our session so we don't get timed out while active
				else {
					$authenticationService->renewSession();
				}
			}
			// if they are authenticated and browsing the public side, renew their session too
			elseif ($authenticationService->isAuthenticated()) {
				$authenticationService->renewSession();
			}
			
			return true;
		}
	
		public function getConfig() {
			return include __DIR__ . '/config/module.config.php';
		}
	
		public function getAutoloaderConfig() {
			return array(
				'Zend\Loader\StandardAutoloader' => array(
					'namespaces' => array(
						__NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
					),
				),
			);
		}
	}
?>

The attach statement lets us tell Zend to perform some action on ever event call.  It takes three parameters: the event name, what action to perform, and the priority to give this attachment.  In this case, we use MvcEvent::EVENT_DISPATCH as our event "name" to indicate to do this whenever it is ready to dispatch a route to a controller/action. For the action, we have it call a separate function for easier code reading.  You could also do a function() there with the same code.  The attachment priority determines where it will fall within the internal listener stack – higher numbers execute first. 1 is the default so we could just leave that off, if desired. It seemed like the priority is really only important if you are making multiple attachments.

The code within the authorizationCheck() function is relatively straight forward – if the user is on the public don't worry about doing authorization stuff other than renewing their current session.  If they are in an authenticated area, check to see if they are logged in and either redirect to the login if they are not or let them continue on their content after renewing the session.  The rest of this file is your standard fare stuff from a Module.php file.

The module.config.php is again relatively standard, with our routes defined, view_manager set, and our factories and controller set up.

<?php
	return array(
		'service_manager' => array(
			'factories' => array(
				'LoginForm' => function ($sm) {
					$form = new Authentication\LoginForm();
					return $form;
				},
				'AuthenticationService' => function ($sm) {
					$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
					$service = new Authentication\AuthenticationService($dbAdapter);
					
					return $service;
				},
			),
			'aliases' => array(
				'Zend\Authentication\AuthenticationService' => 'my_auth_service',
			),
			'invokables' => array(
				'my_auth_service' => 'Zend\Authentication\AuthenticationService',
			),
		),
		'controllers' => array(
			'invokables' => array(
				'AuthenticationController' => 'Authentication\AuthenticationController',
			),
		),
		'router' => array(
			'routes' => array(
				'authentication' => array(
					'type'	=> 'Literal',
					'options' => array(
						'route'	=> '/authentication',
						'defaults' => array(
							'__NAMESPACE__' => 'Authentication',
							'controller'	=> 'AuthenticationController',
							'action'		=> 'login',
						),
					),
					'may_terminate' => true,
					'child_routes' => array(
						'default' => array(
							'type'	=> 'Segment',
							'options' => array(
								'route'	=> '/[/:action]',
								'constraints' => array(
									'action'	 => '[a-zA-Z][a-zA-Z0-9_-]*',
								),
								'defaults' => array(),
							),
						),
					),
				), 	    
				'login' => array(
					'type' => 'Literal',
					'options' => array(
						'route'	=> '/login',
						'defaults' => array(
							'__NAMESPACE__' => 'Authentication',
							'controller'	=> 'AuthenticationController',
							'action'		=> 'login',
						),
					),
				),
				'logout' => array(
					'type' => 'Literal',
					'options' => array(
						'route'	=> '/logout',
						'defaults' => array(
							'__NAMESPACE__' => 'Authentication',
							'controller'	=> 'AuthenticationController',
							'action'		=> 'logout',
						),
					),
				), 
			),
		),
		'view_manager' => array(
			'template_path_stack' => array(
				__DIR__ . '/../view',
			),
		),
	);
?>

The one odd bit is this:

'aliases' => array(
	'Zend\Authentication\AuthenticationService' => 'my_auth_service',
),
'invokables' => array(
	'my_auth_service' => 'Zend\Authentication\AuthenticationService',
),

I honestly cannot explain to you why this is necessary, only that it is – exactly as written.  Without it, I was unable to access the user's authentication status from the layout or views.  The Zend docs just tell you to do it as well, and that it's necessary for using the Identity view helper.  In this case, I shrugged and said fine, added it, and poof, happy system. 🙂

The AuthenticationController just has two actions – loginAction and logoutAction. In both cases, it passes off to our service for most of the work.

<?php
	namespace Authentication;
	
	use Zend\Mvc\Controller\AbstractActionController;
	use Zend\View\Model\ViewModel;
	
	class AuthenticationController extends AbstractActionController {
		protected $authenticationService;
		
		public function loginAction() {
			$authenticationService = $this->getServiceLocator()->get('AuthenticationService');
			
			$form = $this->getServiceLocator()->get('LoginForm');
			
			$request = $this->getRequest();
			
			// if this is a form submission, it's a log in attempt to validate the form
			if ($request->isPost()) {
				$form->setData($request->getPost());
				
				// form is valid? cool, on to the authenticationService to actually perform the log in attempt
				if ($form->isValid()) {
					$data = $form->getData();
					$aAuthResults = $authenticationService->authenticate($data['username'], $data['password']);
					
					// authentication failed? set up our return response
					if (!$aAuthResults->isValid()) {
						$this->flashMessenger()->setNamespace("error")->addMessage('Invalid login');
					} else {
						$this->flashMessenger()->setNamespace("success")->addMessage('Logged in successfully!');
						$this->redirect()->toRoute('adminHome');
					}
				}
			}
			return array('form' => $form);
		}
		
		// log out is easy - call the log out function of our service and pass them to publicHome
		public function logoutAction() {
			$authenticationService = $this->getServiceLocator()->get('AuthenticationService');
			
			$authenticationService->logout();
			$this->flashmessenger()->setNamespace("success")->addMessage("You've been logged out");
			$this->redirect()->toRoute('publicHome');	
		}
	}
?>

The loginAction does also serve up the log in form, if the user isn't authenticated yet or if their log in failed.  As with most actions that may be receiving a post submission, it passes to the form validator first and makes sure it's valid before sending off to our authentication service to check the credentials. If authentication fails, it gives the user an error message and returns them back to the form.  If it works, we take them to our Admin home.

My AuthenticationService.php is loosely based on the base Zend one, but customized for my purposes. We'll be using the DBAdapter to authenticate a user name/password against the database, and sessions for managing the user's log in status.

<?php
	namespace Authentication;
	
	use Zend\ServiceManager\ServiceLocatorAwareInterface;
	use Zend\Db\Sql\Sql;
	use Zend\Authentication\Storage\Session;
	use Zend\Authentication\Adapter\DbTable as AuthAdapter;
	
	class AuthenticationService implements ServiceLocatorAwareInterface {
		protected $storage = null;
		protected $dbAdapter;
		protected $sql;
		protected $idleTimeOut;
		
		// construct sets up our dbAdapter and idleTimeout variables
		public function __construct($dbAdapter) {
			$this->setDbAdapter($dbAdapter);
			$this->sql = new Sql($dbAdapter);
			
			$this->idleTimeOut = 60*10; //time out in 10 minutes
		}
		
		// perform authentication function using Zend's DbTable adapater
		public function authenticate($username, $password) {
			$dbAdapter = $this->getDbadapter();
			
			// must give the adapter a valid dbAdapter!
			$authAdapter = new AuthAdapter($dbAdapter);
			
			// set up our configuration stuff - which table to use, which fields, and what variables to match against them
			$authAdapter
				->setTableName('admins')
				->setIdentityColumn('username')
				->setCredentialColumn('password')
				->setIdentity($username)
				->setCredential(md5($password))
			;
			
			$result = $authAdapter->authenticate();
			
			// if they tried to log in twice somehow - log them out first then log them in again
			if ($this->isAuthenticated()) {
				$this->logout();
			}
			
			// if authentication was successful, pass the username to our storage (session) for saving and set the session timeout
			if ($result->isValid()) {
				$this->getStorage()->write($result->getIdentity());
				$_SESSION['timeout_idle'] = time();
			}
			
			return $result;
		}
		
		// on log out, tell our storage to clear the session variables
		public function logout() {
			 $this->getStorage()->clear();
		}
				
		// called to extend the time out if the users is navigating around the site; also checks to make sure they didn't log out while sitting idle before they navigated again
		public function renewSession() {
			$storage = $this->storage;
			
			if(isset($_SESSION['timeout_idle'])) {
				//extract the data to local variables to make things easier to understand
				$stored = $_SESSION['timeout_idle'];
				$expires = $stored + $this->idleTimeOut;
			
				if ($expires < time()) {
					$this->logout();
				}
			}

			$_SESSION['timeout_idle'] = time();
		}
		
		// our getters and setters
		public function isAuthenticated() {
			return !$this->getStorage()->isEmpty();
		}
		
		public function getIdentity() {
			$storage = $this->getStorage();
			
			if ($storage->isEmpty()) {
				return null;
			}
			
			return $storage->read();
		}
		
		public function getDbadapter() {
			return $this->dbAdapter;	
		}
		public function setDbadapter($dbAdapter) {
			$this->dbAdapter = $dbAdapter;	
		}
		
		public function getStorage() {
			if (null === $this->storage) {
				$this->setStorage(new Session());
			}
			
			return $this->storage;
		}
		
		public function setStorage($storage) {
			$this->storage = $storage;
			return $this;
		}
		
		public function getServiceLocator() {
			return $this->serviceLocator;	
		}
		
		public function setServiceLocator(\Zend\ServiceManager\ServiceLocatorInterface $serviceLocator) {
			$this->serviceLocator = $serviceLocator;	
		}
	}
?>

As far as I could find, Zend does not automatically deal with session renewals even when using their authentication stuff.  It does correctly set things up so a user is logged out on browser close, but it doesn't automatically time them out nor does it keep the session alive while users are navigating around if you do set up a time out.  Because of this, I added the manual setting of the $_SESSION['timeout_idle'] when logging in, and added a renewSession function that is called by Module.php every time an authenticated user moves around the site.  This function confirms they have not timed out – if they haven't, it extends the time out. If they have, it logs them out completely so they will get kicked back to the log out screen.

I'd imagine there are other/better ways to to this, but so far, this method seems to be working *cross fingers*

The last two bits of this module are, of course, the LoginForm and the login.phtml view.  Both are pretty simple and self explanatory, I think:

<?php
	namespace Authentication;
	
	use Zend\Form\Element;
	use Zend\Form\Form;
	
	class LoginForm extends Form {
		public function __construct() {
			// now to the form
			parent::__construct('login');
			
			$this->setAttribute('method', 'post')
				->setAttribute('class', 'pure-form pure-form-aligned standardForm');
			
			$element = new Element\Text('username');
			$element->setLabel('User Name')
				->setAttributes(
					array(
						'id' => 'username',
						'class' => 'size30',
						'required' => 'required',
						)
					)
				->setLabelAttributes(array('class' => 'required'));
			
			$this->add($element);
			
			$element = new Element\Password('password');
			$element->setLabel('Password')
				->setAttributes(
					array(
						'id' => 'password',
						'class' => 'size30',
						'required' => 'required',
						)
					)
				->setLabelAttributes(array('class' => 'required'));
			
			$this->add($element);
			
			$button = new Element\Button('saveButton');
			$button->setLabel("Save");
			$button->setAttributes(
				array(
					'id' => 'saveButton',
					'type' => 'submit',
					'value' => 'Log In',
				)
			);
			$this->add($button);
			
		}
		
		
	}
?>
<?php
	$title = 'Log In';
	$this->headTitle($title);
	$this->layout()->pageTitle = $title;

	$this->form->setAttribute('action', $this->url('authentication'));
	$this->form->prepare();
	
	echo $this->form($form);
?>

The final touch is, of course, to include our Module in our application.config.php. Then any attempt to access any non-public page (which right now is all of them), results in a log in screen.  We can test this out a bit though by updating our navigation from the last bit. In my layout.phtml, I added $user = $this->identity(); to the top of the page, then I changed this:

<div id="leftSideBar">
	<?php echo $this->navigation('PublicNavigation')->menu(); ?>
	<hr />
	<?php echo $this->navigation('AdminNavigation')->menu()->setMaxDepth(1); ?>
</div>

To this:

<div id="leftSideBar">
	<?php 
		if(empty($user)) {
			echo $this->navigation('PublicNavigation')->menu();
		}
		else {
			echo $this->navigation('AdminNavigation')->menu()->setMaxDepth(1);
		}
	?>
</div>

And added log in/out links in my header:

<div id="siteHeader">
	<div id="siteNameBox">
		<a href="<?php echo $this->url('publicHome') ?>" title=""><img src="<?php echo $this->basePath('img/header_movieshelves.gif') ?>" width="461" height="100" alt="Take me to the Movie Shelves front page" /></a>
	</div>
	<div id="siteMenu">
		<div id="loginLink">
		<?php 
			if(empty($user)) {
				echo '<a href="' . $this->url('authentication', array('action'=>'login')) . '">Log In</a>';
			}
			else {
				echo '<a href="' . $this->url('logout', array('action'=>'logout')) . '">Log Out</a>';
			}
		?>
		</div>
		Can I get a menu?
	</div>
</div>

So now when I'm logged in, I see a log out link and the admin navigation…

…and if I'm logged out, I see a log in link and our public navigation.

Here is a zip of all the files mentioned above, for easier reference/playing with.