Sunday 21 July 2013

Zend Framework 2: Bcrypt adapter for Zend Authentication component

ZF2 provides us with a class for using bcrypt algorithm for hashing and verifying passwords.

I want to try it with zend authentication component. I'd like to use bcrypt algorithm with dbTable authentication adapter.

Unfortunately, ZF2 has no built-in auth adapter for this.

Ok, let's make our own auth adapter based on DbTable auth adapter.

The logic is:
- fetch all rows from table where identity column value is equal to value of identity field from login form(it could be login name, email, etc)
- check if entered password is valid using bcrypt->verify() method

Implementation:

1. Create class that extends DbTable auth adapter

// define namespace for the class
namespace SomeModule\Auth\Adapter;

class BcryptDbAdapter extends DbTable
{
}

2. It is necessary to redefine two methods in our new class.
First method: authenticateCreateSelect() - method that creates a Zend\Db\Sql\Select object for fetching data from database
Second method: authenticateQuerySelect() - method that fetches data from database using Zend\Db\Sql\Select object from previous method.

Our class will look like below:

namespace SomeModule\Auth\Adapter;

use Zend\Authentication\Adapter\DbTable;
use Zend\Db\Sql;
use Zend\Db\Sql\Predicate\Operator as SqlOp;

class BcryptDbAdapter extends DbTable
{
    protected function authenticateCreateSelect()
    {
        // get select
        $dbSelect = clone $this->getDbSelect();
        $dbSelect->from($this->tableName)
            ->columns(array('*'))
            ->where(new SqlOp($this->identityColumn, '=', $this->identity));

        return $dbSelect;
    }

    protected function authenticateQuerySelect(Sql\Select $dbSelect)
    {
        $sql = new Sql\Sql($this->zendDb);
        $statement = $sql->prepareStatementForSqlObject($dbSelect);

        try {
            $result = $statement->execute();
            $resultIdentities = array();

            // create object ob Bcrypt class
            $bcrypt = new \Zend\Crypt\Password\Bcrypt();

            // iterate result, most cross platform way
            foreach ($result as $row) {
                if ($bcrypt->verify($this->credential, $row[$this->credentialColumn])) {
                    $row['zend_auth_credential_match'] = 1;
                    $resultIdentities[] = $row;
                }
            }

        } catch (\Exception $e) {
            throw new Exception\RuntimeException(
                'The supplied parameters to DbTable failed to '
                    . 'produce a valid sql statement, please check table and column names '
                    . 'for validity.', 0, $e
            );
        }

        return $resultIdentities;
    }
}
3. How to use example.

- Add required namespaces to controller:
use SomeModule\Auth\Adapter\BcryptDbAdapter as AuthAdapter;
use Zend\Authentication\AuthenticationService;

- Authenticate user in login action:

$data = $request->getPost();

$dbAdapter = $this->getServiceLocator()->get('Zend\Db\Adapter\Adapter');

$authAdapter = new AuthAdapter($dbAdapter);

$authAdapter
  ->setTableName('users')
  ->setIdentityColumn('email')
  ->setCredentialColumn('password');

$authAdapter
  ->setIdentity(addslashes($data['email']))
  ->setCredential($data['password']);

// attempt authentication
$result = $authAdapter->authenticate();

if (!$result->isValid()) {
  // Authentication failed
} else {
  $auth = new AuthenticationService();
  $storage = $auth->getStorage();

  $storage->write($authAdapter->getResultRowObject(
    null,
    'password'
  ));
}

Source code is available on GitHub