Yii 1.1: Authenticating against phpass hashes with Yii

30 followers

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

Total 18 comments

#12749 report it
AustinGeek at 2013/04/09 05:25pm
Thanks for the extension

I played with a few different extensions but think this makes sense.

Should it update non-encrypted passwords with the new hash? It seems like it hits the "Password Incorrect" Error and stops.

Do I need to apply a hash to all my data first?

It seems like this was meant to deal with unhashed passwords but it doesn't work for me.

I removed the md5 reference as my passwords were unhashed initially, that seems to allow the upgrade to hashed passwords without users knowing.
Thanks again! 
//Is this a vanilla hash?
    if($record->password{0}!=='$')
    {
      $record->password=$ph->HashPassword($this->password);
      $record->save();
    }

#12422 report it
grmpz at 2013/03/19 05:48pm
Also had to declare password1, password2 safe

FWIW I also had to add array('password1, password2', 'safe'), to the rules using yii 1.1.13. Without that line $this->password1 and $this->password2 are always blank in beforeSave. Adding just password1 as safe would not update the password successfully.

Thanks for a great article. Very informative and easy to implement.

*Edit: It may be worth noting that I integrated the above into the yii-user extenstion (http://www.yiiframework.com/extension/yii-user/)

#12004 report it
Da:Sourcerer at 2013/02/20 03:53am
RE: password1 & password2 safe?

Uhm, well ... attributes are considered safe if they are covered by any rule in the current scenario. Could be that this is what is happening with password1. Could you try to declare just the password1 attribute as safe?

#11332 report it
Daniel at 2013/01/06 06:09pm
password1 & password2 safe?

Hi,

I was wondering, do I need to add this in rule, in addition to compare line?

public function rules()
    {
        // NOTE: you should only define rules for those attributes that
        // will receive user inputs.
        return array(
            ...
            array('password2', 'compare', 'compareAttribute'=>'password1'),
            array('password1, password2', 'safe'),
            ...
        );
    }

Without declaring as safe, I cannot go inside the if body although meet the condition.

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();
    }

Thank you for the great and helpful wiki!

Daniel

#9184 report it
bonnie at 2012/07/26 08:27pm
Login to production issues

Need help if anyone has had this problem after configuring phpass on my yii app and tested on my local machine am able to login but moving to production server it wont log me in. Tried my other host and works fine. The prod is running on php5.2 and the local and other hosting are running php5.3. Thanks.

#8425 report it
bonnie at 2012/06/01 05:29pm
Will check

Will check and still in dev

#8424 report it
Da:Sourcerer at 2012/06/01 04:23pm
RE: Thanks for nice extension

That's mighty strange. You should better check which rule it is that's missfiring (if it is missfiring) in User.rules(). Just using save(false) is no good workaround.

#8406 report it
bonnie at 2012/05/31 06:30pm
Thanks for nice extension
//Is this a vanilla hash?
    if($record->password{0}!=='$')
    {
      $record->password=$ph->HashPassword($this->password);
      $record->save();
    }

Trying to encrypt existing passwords with this method. I had issues with my app until I had to add false in the save method. As in

//Is this a vanilla hash?
    if($record->password{0}!=='$')
    {
      $record->password=$ph->HashPassword($this->password);
      $record->save(false);
    }

Though thanks for your effort.

#7065 report it
Da:Sourcerer at 2012/02/22 04:46am
RE: Bug in the beforeSave function?

Yes, indeed. Thanks for the pointer. That must have slipped in during clean-up.

#7059 report it
acorncom at 2012/02/21 06:05pm
Bug in the beforeSave function?

Shouldn't the following line in your beforeSave example be updated from this:

$this->password=$ph->HashPassword($this->password);

to this?

$this->password=$ph->HashPassword($this->password1); // note the 1

Because you want to test the passwords against themselves and then, if they match, you want to hash the password and then save the hash into the password variable. Correct?

#6799 report it
Da:Sourcerer at 2012/02/06 05:21am
RE: open_basedir on /dev/urandom call

Thanks for reporting this. This hasn't come to my attention either. But it looks like the authors took care of that about 10 months ago. Their fix is identical to yours. Unfortunately, it hasn't made it in any release yet.

#6740 report it
stereochrome at 2012/02/02 02:53am
open_basedir on /dev/urandom call

Hi,

a customer stumbled across a problem with phpass today: the extension tries to check if /dev/urandom is readable (File PasswordHash.php, Line 51). If the server enforces an open_basedir setting, the call to is_readable will throw an php error (open_basedir restriction in effect - you do not want to add /dev to open_basedir!) which breaks the application.

This slipped through my test environment as it is not using open_basedir :(

Add an @ before the is_readable call to fix this problem.

I will try to contact the original author of this file about this problem.

#5706 report it
Da:Sourcerer at 2011/11/04 11:12am
RE: Check hash success

I see. It would still be interesting to know how often that happens.

#5705 report it
trenchard at 2011/11/04 10:55am
RE: Check hash success

It is simply a good practice in our development model to catch the "black swan" unexpected events especially when it comes to security

#5704 report it
Da:Sourcerer at 2011/11/04 06:26am
RE: Check hash success

Hm, interesting. How often does hashing fail? I've always thought of phpass to be quite failsafe.

#5701 report it
trenchard at 2011/11/03 11:31pm
Check hash success

There is an easy way to ensure the hash was successful. Here is a sample method we use...

public function securePassword($password)
{
    //Instantiate object with configuration
    $passwordHash = new PasswordHash(Yii::app()->params['phpass']['iteration_count)_log2'],Yii::app()->params['phpass']['portable_hashes']);
     //Hash the passed password
     $securedPassword = $passwordHash->HashPassword($password);
     unset($passwordHash);
 
     If (strlen($securedPassword < 20))  //smallest hash is 20 characters
     {
         return false;
     } 
     else
     {
         return $securedPassword;
     }
}
#5187 report it
Da:Sourcerer at 2011/09/21 01:06pm
Problem...

In hindsight, the solution with User.beforeSave() isn't that great. If an existing User would be updated, the hashed password might be hashed again, thus making logging in impossible. I'll need to refine that example soon.

Edit: Should be all good now.

#5182 report it
Alexandre Carmo at 2011/09/21 11:33am
I like this

This is a good option..... I gonna test

Leave a comment

Please to leave your comment.

Write new article