Authenticating against phpass hashes with Yii

  1. Preface
  2. Installation
  3. Using existing hashes
  4. Links

Note: This guide is outdated as of Yii v1.1.14 which introduced the [CPasswordHelper] class. Please use that instead.

Preface

The Portable PHP password hashing framework allows advanced password hashing offering increased security over simple MD5- or SHA1-hashed passwords. phpass is already in use in some larger projects such as WordPress (since v2.5), Drupal 7 and phpBB 3.

Installation

Fetch the latest version of phpass from here (At the time of this writing, that is v0.3), and extract the contained PasswordHash.php to application.extensions.

Preparing Yii Configuration

Open your application/config/main.php and locate the import stanza. Add another entry like this:

'import'=>array(
  ...,
  'application.extensions.PasswordHash',
),

Then, locate the params stanza and add a new configuration hash for phpass:

// application-level parameters that can be accessed
// using Yii::app()->params['paramName']
'params'=>array(
  ...,
  'phpass'=>array(
    'iteration_count_log2'=>8,
    'portable_hashes'=>false,
  ),
),

This will allow to change the phpass configuration rapidly. For reference:

  • iteration_count_log2 controls the number of iterations for key stretching. A setting of 8 means the hash algorithm will be applied 2^8 = 256 times. This setting should be kept between 4 and 31.
  • portable_hashes controls whether portable hashes should be used or not. Portable hashes are salted MD5 hashes prefixed by $P$.
Preparing UserIdentity

The following snippet is a modified UserIdentity class taken from the Definite Guide to Yii section 8.3: Authentication and Authorization - Defining Identity Class.

class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        $record=User::model()->findByAttributes(array('username'=>$this->username));
        $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']);
        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if(!$ph->CheckPassword($this->password, $record->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->errorCode=self::ERROR_NONE;
        }
        return !$this->errorCode;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}

This will check submitted passwords against a hash stored in database. Now we just need to create those hashes.

Preparing the User model

We need to overwrite the [CActiveRecord::beforeSave()] method so we can hash the password before it gets sent to the database:

class User extends CActiveRecord
{
  public $password1;
  public $password2;
  ...
  public function beforeSave()
  {
    if(!empty($this->password1) && $this->password1==$this->password2)
    {
      $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']);
      $this->password=$ph->HashPassword($this->password1);
    }
    return parent::beforeSave();
  }
}

In the example above, User.password1 and User.password2 are expected to be filled by a form updating the user's password. This is in an effort to keep existing users from being crippled by updates that do not involve password changes. In addition, you might want to add the following validation rule:

public function rules()
{
  return array(
    ...,
    array('password2', 'compare', 'compareAttribute'=>'password1'),
  );
}
Caveats

If you are storing your users in a database, see to it that the password field has sufficient length (at least 60 characters). Otherwise you'll be risking truncated, invalid hashes.

Using existing hashes

phpass is using salted hashes exclusively and is using a custom base64 scheme. So there is no way to "upgrade" existing hashes to phpass hashes. However, since the output of phpass looks entirely different than the output of md5(), sha1(), etc., it is possible to use both schemes in parallel. Just replace the line

else if(!$ph->CheckPassword($this->password, $record->password))

in UserIdentity with:

else if(md5($this->password)!==$record->password && !$ph->CheckPassword($this->password, $record->password))

Of course, you can also try to set a seamless upgrade mechanism in place by creating a new hash every time a user logs in with his correct credentials that are still using the old scheme. Just replace the UserIdentity::authenticate() method with this:

public function authenticate()
{
  $record=User::model()->findByAttributes(array('username'=>$this->username));
  $ph=new PasswordHash(Yii::app()->params['phpass']['iteration_count_log2'], Yii::app()->params['phpass']['portable_hashes']);
  if($record===null)
    $this->errorCode=self::ERROR_USERNAME_INVALID;
  else if(md5($this->password)!==$record->password && !$ph->CheckPassword($this->password, $record->password))
    $this->errorCode=self::ERROR_PASSWORD_INVALID;
  else
  {
    //Is this a vanilla hash?
    if($record->password{0}!=='$')
    {
      $record->password=$ph->HashPassword($this->password);
      $record->save();
    }
    $this->_id=$record->id;
    $this->errorCode=self::ERROR_NONE;
  }
  return !$this->errorCode;
}

Links

22 0
28 followers
Viewed: 39 232 times
Version: 1.1
Category: How-tos
Written by: Da:Sourcerer
Last updated by: Da:Sourcerer
Created on: Sep 21, 2011
Last updated: 9 years ago
Update Article

Revisions

View all history

Related Articles