Using PHPass password hasher with Zend Framework

In this tutorial, we will learn how to use PHPass, a portable PHP password hashing component, with the “glue”-based Zend Framework.  The popular PHP-based blogging platform WordPress utilizes PHPass to hash user passwords.  Using this library adds an extra level of security over a normal MD5-protected password.

For the sake of simplicity, I will not delve into the Zend Framework’s default directory structure.  In this tutorial, we will focus on integration of PHPass into ZF, with the assumption that you already have a basic knowledge of Zend Framework.  The files and directories referenced in this tutorial are listed below, as well as framework version information:

  • /application/controllers/AuthController.php
  • /forms/Login.php
  • /models/Model_Users.php
  • /views/scripts/auth/index.phtml
  • /library/Webjawns/Auth/Adapter.php
  • /library/Webjawns/Auth/PasswordHash.php
  • /library/Webjawns/Db/Table/Abstract.php
  • PHPass – PasswordHash.php – version 0.2
  • Zend Framework - version 1.9.3PL1

/library/Webjawns/Auth/PasswordHash.php

This file contains the brain of this operation.  This is the PHPass library which contains the logic for encrypting and checking passwords.

#
# Portable PHP password hashing framework.
#
# Version 0.2 / webjawns - zf and phpass tutorial.
#
# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
# the public domain.
#
# There's absolutely no warranty.
#
# The homepage URL for this framework is:
#
#	http://www.openwall.com/phpass/
#
# Please be sure to update the Version line if you edit this file in any way.
# It is suggested that you leave the main version number intact, but indicate
# your project name (after the slash) and add your own revision information.
#
# Please do not change the "private" password hashing method implemented in
# here, thereby making your hashes incompatible.  However, if you must, please
# change the hash type identifier (the "$P$") to something different.
#
# Obviously, since this code is in the public domain, the above are not
# requirements (there can be none), but merely suggestions.
#
class Webjawns_Auth_PasswordHash {
	var $itoa64;
	var $iteration_count_log2;
	var $portable_hashes;
	var $random_state;

	function Webjawns_Auth_PasswordHash($iteration_count_log2, $portable_hashes)
	{
		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

		if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
			$iteration_count_log2 = 8;
		$this->iteration_count_log2 = $iteration_count_log2;

		$this->portable_hashes = $portable_hashes;

		$this->random_state = microtime() . getmypid();
	}

	function get_random_bytes($count)
	{
		$output = '';
		if (is_readable('/dev/urandom') &&
		    ($fh = @fopen('/dev/urandom', 'rb'))) {
			$output = fread($fh, $count);
			fclose($fh);
		}

		if (strlen($output) < $count) {
			$output = '';
			for ($i = 0; $i < $count; $i += 16) {
				$this->random_state =
				    md5(microtime() . $this->random_state);
				$output .=
				    pack('H*', md5($this->random_state));
			}
			$output = substr($output, 0, $count);
		}

		return $output;
	}

	function encode64($input, $count)
	{
		$output = '';
		$i = 0;
		do {
			$value = ord($input[$i++]);
			$output .= $this->itoa64[$value & 0x3f];
			if ($i < $count)
				$value |= ord($input[$i]) << 8;
			$output .= $this->itoa64[($value >> 6) & 0x3f];
			if ($i++ >= $count)
				break;
			if ($i < $count)
				$value |= ord($input[$i]) << 16;
			$output .= $this->itoa64[($value >> 12) & 0x3f];
			if ($i++ >= $count)
				break;
			$output .= $this->itoa64[($value >> 18) & 0x3f];
		} while ($i < $count);

		return $output;
	}

	function gensalt_private($input)
	{
		$output = '$P$';
		$output .= $this->itoa64[min($this->iteration_count_log2 +
			((PHP_VERSION >= '5') ? 5 : 3), 30)];
		$output .= $this->encode64($input, 6);

		return $output;
	}

	function crypt_private($password, $setting)
	{
		$output = '*0';
		if (substr($setting, 0, 2) == $output)
			$output = '*1';

		if (substr($setting, 0, 3) != '$P$')
			return $output;

		$count_log2 = strpos($this->itoa64, $setting[3]);
		if ($count_log2 < 7 || $count_log2 > 30)
			return $output;

		$count = 1 << $count_log2;

		$salt = substr($setting, 4, 8);
		if (strlen($salt) != 8)
			return $output;

		# We're kind of forced to use MD5 here since it's the only
		# cryptographic primitive available in all versions of PHP
		# currently in use.  To implement our own low-level crypto
		# in PHP would result in much worse performance and
		# consequently in lower iteration counts and hashes that are
		# quicker to crack (by non-PHP code).
		if (PHP_VERSION >= '5') {
			$hash = md5($salt . $password, TRUE);
			do {
				$hash = md5($hash . $password, TRUE);
			} while (--$count);
		} else {
			$hash = pack('H*', md5($salt . $password));
			do {
				$hash = pack('H*', md5($hash . $password));
			} while (--$count);
		}

		$output = substr($setting, 0, 12);
		$output .= $this->encode64($hash, 16);

		return $output;
	}

	function gensalt_extended($input)
	{
		$count_log2 = min($this->iteration_count_log2 + 8, 24);
		# This should be odd to not reveal weak DES keys, and the
		# maximum valid value is (2**24 - 1) which is odd anyway.
		$count = (1 << $count_log2) - 1;

		$output = '_';
		$output .= $this->itoa64[$count & 0x3f];
		$output .= $this->itoa64[($count >> 6) & 0x3f];
		$output .= $this->itoa64[($count >> 12) & 0x3f];
		$output .= $this->itoa64[($count >> 18) & 0x3f];

		$output .= $this->encode64($input, 3);

		return $output;
	}

	function gensalt_blowfish($input)
	{
		# This one needs to use a different order of characters and a
		# different encoding scheme from the one in encode64() above.
		# We care because the last character in our encoded string will
		# only represent 2 bits.  While two known implementations of
		# bcrypt will happily accept and correct a salt string which
		# has the 4 unused bits set to non-zero, we do not want to take
		# chances and we also do not want to waste an additional byte
		# of entropy.
		$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

		$output = '$2a$';
		$output .= chr(ord('0') + $this->iteration_count_log2 / 10);
		$output .= chr(ord('0') + $this->iteration_count_log2 % 10);
		$output .= '$';

		$i = 0;
		do {
			$c1 = ord($input[$i++]);
			$output .= $itoa64[$c1 >> 2];
			$c1 = ($c1 & 0x03) << 4;
			if ($i >= 16) {
				$output .= $itoa64[$c1];
				break;
			}

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 4;
			$output .= $itoa64[$c1];
			$c1 = ($c2 & 0x0f) << 2;

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 6;
			$output .= $itoa64[$c1];
			$output .= $itoa64[$c2 & 0x3f];
		} while (1);

		return $output;
	}

	function HashPassword($password)
	{
		$random = '';

		if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) {
			$random = $this->get_random_bytes(16);
			$hash =
			    crypt($password, $this->gensalt_blowfish($random));
			if (strlen($hash) == 60)
				return $hash;
		}

		if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) {
			if (strlen($random) < 3)
				$random = $this->get_random_bytes(3);
			$hash =
			    crypt($password, $this->gensalt_extended($random));
			if (strlen($hash) == 20)
				return $hash;
		}

		if (strlen($random) < 6)
			$random = $this->get_random_bytes(6);
		$hash =
		    $this->crypt_private($password,
		    $this->gensalt_private($random));
		if (strlen($hash) == 34)
			return $hash;

		# Returning '*' on error is safe here, but would _not_ be safe
		# in a crypt(3)-like function used _both_ for generating new
		# hashes and for validating passwords against existing hashes.
		return '*';
	}

	function CheckPassword($password, $stored_hash)
	{
		$hash = $this->crypt_private($password, $stored_hash);
		if ($hash[0] == '*')
			$hash = crypt($password, $stored_hash);

		return $hash == $stored_hash;
	}
}

/library/Webjawns/Auth/Adapter.php

Unfortunately, because we are using a custom authorization system that involves more than just matching a password hash to a database row, we cannot use the default Zend Framework Zend_Auth_Db_Adapter class.  The Webjawns_Auth_Adapter class allows us to link the functionality of PHPass with Zend_Auth so we can have the best of both worlds.

/**
 * Webjawns_Auth_Adapter
 * 
 * @package Webjawns
 * @subpackage Auth
 */
class Webjawns_Auth_Adapter implements Zend_Auth_Adapter_Interface {
	/**
	 * The username
	 * 
	 * @var string
	 */
	protected $_identity = null;
	
	/**
	 * The password
	 * 
	 * @var string
	 */
	protected $_credential = null;
	
	/**
	 * Users database object
	 * 
	 * @var Model_Db_Table_Users
	 */
	protected $_usersModel = null;
	
	public function __construct($username, $password, $usersModel) {
		if (!$usersModel instanceof Webjawns_Db_Table_Abstract) {
			throw new Zend_Auth_Exception('No adapter found for $usersModel');
		}
		
		$this->_identity = $username;
		$this->_credential = $password;
		$this->_usersModel = $usersModel;
	}

	public function authenticate() {
		// Fetch user information according to username
		if (!$user = $this->_usersModel->getUserBy('login', $this->_identity)) {
			return new Zend_Auth_Result(
				Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND,
				$this->_identity,
				array('Invalid username')
			);
		}
		
		// Pass given credential through PHPass and check whether or not the hash matches
		if (!$this->_usersModel->checkPassword($this->_credential, $user->user_password, $user->user_id)) {
			return new Zend_Auth_Result(
				Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID,
				$this->_identity,
				array('Incorrect password')
			);
		}
		
		// Success!
		return new Zend_Auth_Result(
			Zend_Auth_Result::SUCCESS,
			$this->_identity,
			array()
		);
	}
	
}

/library/Webjawns/Db/Table/Abstract.php

This class is not necessarily required, but developers often utilize a database structure that involves table prefixes.  While Zend Framework does not offer this functionality out of the box, it is possible to still add table prefixes.  This class does just that…

/**
 * Handles setting up table prefix for all tables.
 * 
 * @package Webjawns
 */
class Webjawns_Db_Table_Abstract extends Zend_Db_Table_Abstract {
	/**
	 * The table prefix
	 * 
	 * @access protected
	 * @var string
	 */
	protected $table_prefix = null;
	
	/**
	 * Sets up table prefix for all tables extending this class.
	 * 
	 * @access protected
	 * @return void
	 */
	protected function _setupTableName() {
		parent::_setupTableName();
		
		$this->table_prefix = 'webjawns_'; /* if using registry: Zend_Registry::get('table_prefix') */
		$this->_name = $this->table_prefix . $this->_name;
	}
}

/application/models/Users.php

Model_Users provides a direct connection to the user database table, and contains the functions for comparing, and setting hashed passwords.  You may notice a parallel between the functions here and some of those used in WordPress.  We used the same authentication concepts to design Model_Users.

/**
 * Model_Users
 * 
 * Example database structure:
 *  'user_id' => The user row ID
 * 	'user_name' => The username
 * 	'user_password' => The user's password
 *
 * @package Webjawns
 */
class Model_Users extends Webjawns_Db_Table_Abstract {
	
	protected $_name = 'users';
	
	/**
	 * Get user by a particular field.
	 * 
	 * @param string $field
	 * @param string $value
	 * @return object User row object
	 */
	public function getUserBy($field, $value) {
		switch ($field) {
			case 'login':
				$field = 'user_name';
				break;
			default:
				return false;
		}
		
		if (!$user = $this->fetchRow("$field = '$value'"))
			return false;
		
		return $user;
	}
	
	/**
	 * Create a haash (encrypt) of a plain text password.
	 * 
	 * @param string $password Plain text user password to hash
	 * @return string The hash string of the password
	 */
	public function hashPassword($password) {
		return $this->hasher()->HashPassword($password);
	}
	
	/**
	 * Compare the plain text password with the $hashed password.
	 * 
	 * @param string $password
	 * @param string $hash The hashed password
	 * @param int $user_id The user row ID
	 * @return bool True if match, false if no match.
	 */
	public function checkPassword($password, $hash, $user_id = '') {
		// Check if we are still using regular MD5 (32 chars)
		if (strlen($hash) <= 32) {
			$check = ($hash == md5($password));
			if ($check && $user_id) {
				// Rehash using new PHPass-generated hash
				$this->setPassword($password, $user_id);
				$hash = $this->hashPassword($password);
			}
		}
		
		$check = $this->hasher()->CheckPassword($password, $hash);
		
		return $check;
	}
	
	/**
	 * Set password for specified user ID.
	 * 
	 * @param string $password The user's password (plain text).
	 * @param int $user_id The user row ID.
	 * @return void
	 */
	public function setPassword($password, $user_id) {
		$hash = $this->hashPassword($password);
		
		$this->update(
			array('user_password' => $hash),
			array('user_id' => $user_id)
		);
	}
	
	/**
	 * Checks for Webjawns_PasswordHash in registry. If not present, creates PHPass object.
	 * 
	 * @uses Zend_Registry
	 * @uses Webjawns_PasswordHash
	 * 
	 * @return Webjawns_PasswordHash PHPass
	 */
	public function hasher() {
		if (!Zend_Registry::isRegistered('wj_hasher')) {
			Zend_Registry::set('wj_hasher', new Webjawns_Auth_PasswordHash(8, true));
		}
		return Zend_Registry::get('wj_hasher');
	}
	
}

/application/forms/Login.php

This is the login form.  Simple and sweet!

/**
 * Webjawns Login Form
 *
 * @package Webjawns
 * @subpackage Auth
 */
class Form_Login extends Zend_Form {
	
	public function init() {
		$username = $this->addElement('text', 'username', array(
			'filters' => array('StringTrim', 'StringToLower'),
			'validators' => array('Alnum', array('StringLength', false, array(3, 20))),
			'required' => true,
			'label' => 'Username:',
		));
		
		$password = $this->addElement('password', 'password', array(
			'filters' => array('StringTrim'),
			'validators' => array('Alnum', array('StringLength', false, array(6, 20))),
			'required' => true,
			'label' => 'Password:',
		));
		
		$login = $this->addElement('submit', 'login', array(
			'required' => false,
			'ignore' => true,
			'label' => 'Login',
		));
		
		// Displays 'authentication failed' message if absolutely necessary
		$this->setDecorators(array(
			'FormElements',
			array('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form')),
			array('Description', array('placement' => 'prepend')),
			'Form'
		));
	}
	
}

/application/controllers/AuthController.php

AuthController was actually mostly borrowed from another tutorial found on the web.  When I come across this page again, I will provide a link.  AuthController is responsible for routing any authentication requests, displaying the form, logging out the user, etc.

/**
 * AuthController
 *
 * @package Webjawns
 * @subpackage Auth
 */
class AuthController extends Zend_Controller_Action {
	/**
	 * Index Action - Displays login form.
	 * 
	 * @return void
	 */
	public function indexAction() {
		$this->view->form = $this->getForm();
	}
	
	/**
	 * Process login request.
	 * 
	 * @return unknown_type
	 */
	public function processAction() {
		$request = $this->getRequest();
		
		// Check if we have a POST request
		if (!$request->isPost())
			return $this->_helper->redirector('index');
		
		// Get form and validate
		$form = $this->getForm();
		if (!$form->isValid($request->getPost())) {
			// Did not pass validation...
			$this->view->form = $form;
			return $this->render('index');
		}
		
		// Get authentication adapter and check credentials
		$adapter = $this->getAuthAdapter($form->getValues());
		$auth = Zend_Auth::getInstance();
		$result = $auth->authenticate($adapter);
		if (!$result->isValid()) {
			// Invalid username/password
			$messages = $result->getMessages();
			$form->setDescription($messages[0]);
			$this->view->form = $form;
			return $this->render('index');
		}
		
		// Authentication complete!
		$this->_helper->redirector('index', 'index');
	}
	
	/**
	 * Unauthenticate the current user session.
	 * 
	 * @return void
	 */
	public function logoutAction() {
		Zend_Auth::getInstance()->clearIdentity();
		$this->_helper->redirector('index');
	}
	
	/**
	 * Retrieves login form object.
	 * 
	 * @return object Form_Login
	 */
	public function getForm() {
		return new Form_Login(array(
			'action' => '/auth/process',
			'method' => 'post'
		));
	}
	
	/**
	 * Check whether or not user is logged in.  If user is logged in
	 * do not show the login form; otherwise, redirect to form.
	 * 
	 * @return unknown_type
	 */
	public function preDispatch() {
		if (Zend_Auth::getInstance()->hasIdentity()) {
			// If user is logged in, don't show login form
			if ('logout' != $this->getRequest()->getActionName()) {
				$this->_helper->redirector('index', 'index');
			}
		} else {
			// If user is not logged in, redirect to login form
			if ('logout' == $this->getRequest()->getActionName()) {
				$this->_helper->redirector('index');
			}
		}
	}
	
	/**
	 * Retrieve user authorization adapter.
	 * 
	 * @param array $params
	 * @return Webjawns_Auth_Adapter
	 */
	public function getAuthAdapter(array $params) {
		return new Webjawns_Auth_Adapter(
			$params['username'],
			$params['password'],
			new Model_Users()
		);
	}
	
}

/application/views/scripts/auth/index.phtml

Our view… displays the login form.  Easy as pie!

<h2>Login</h2>
<?php echo $this->form; ?>

While this tutorial does not provide us with a complete user authentication system, it provides a solid skeleton to get you started.  The next step might be to create methods to add new users, delete users, allow users to register, etc.  PHPass provides us with an extra layer of security over untouched MD5, and this coupled with the power of Zend Framework provides us with an excellent starting point for authentication in many applications.  Please feel free to comment with suggestions, ideas, or anything else that comes to mind.

7 thoughts on “Using PHPass password hasher with Zend Framework”

  1. nvm found the issue:
    your blog did convert the 8) into a smiley for some reason, it’s supposed to be like that:

    if (strlen($salt) != 8) return $output;

  2. hi:
    one question, please: if as we see here Zend’s MVC is being used, then which framework controls the redirects or flow? WordPress API or Zend MVC?
    And I saw this question being posed in other posts you have, but unfortunately with no reply?
    Thank you in advance


  3. jose:

    hi:
    one question, please: if as we see here Zend’s MVC is being used, then which framework controls the redirects or flow? WordPress API or Zend MVC?
    And I saw this question being posed in other posts you have, but unfortunately with no reply?
    Thank you in advance

    Hi Jose,

    WordPress is not being used in this example. WordPress was only cited as an example of a platform that uses PHPass for password hashing. This article covers how to implement PHPass with ZF.

    I hope this helps.

    Thanks,
    Chris

  4. Fatal error: Class ‘Webjawns_Auth_Adapter’ not found in C:xampphtdocsapplicationcontrollersAuthController.php on line 74

    How do I let my controller know about this class?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>