RSS
 
25. Aug. 2011

Working with passwords in CakePHP

25 Aug

The basics

Usually, this is already well known. But.. there are still developers who actually store the password unhashed. You always have to store user passwords hashed. You can either use the still very common sha1 method (which is the cake default as for right now) or switch to more secure measures like bcrypt. If you want to be on the safe side you not only use salts (which is also cake standard) but also a unique salt per user. In case someone actually hacks your DB the information he gets is then usually pretty useless. You can read more about this here.

That also means that the password is a one-way ticket. Due to the technical details on hashs you are not able to recover the password from this hash – hence you always need to display some empty password field which is not prepopulated (see the examples below on how to unset the field from the controller). Most websites also use a confirmation field (second password field below the first one) to make sure, there are no spelling mistakes – since the password fields shood be unreadable using the well-known stars (input type=password).

A clean approach for working with passwords

Most beginners make the mistake to use the raw baked templates and “password” as fieldname for the database field “password” in forms. But this causes quite some issues. For starters it will also hash and save empty strings (no input) and mess up previously entered passwords. So you should never use this field in the forms (except for login, of course). Always use “pwd” or some other temporary field name for your forms which you will then map to the real field in your model’s beforeSave() method:

public function beforeSave($options = array()) {
    parent::beforeSave($options);
    if (!empty($this->data[$this->alias]['pwd'])) {
        $this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['pwd']);
    }
    ...
    return true;
}

Also make sure that you grab and use the password before it gets hashed if you plan on sending an email with the password to the user. After the hashing there is no way in resolving it back to what it used to be (basic principle of hash algorithms).

The main advantage of ALWAYS using a different name (even with cake2) is that you cannot accidently write an unhandled/unhashed password to your database. Also consider providing a “pwd_repeat” field to ensure the entered password was typed correctly – especially on the user side. To keep this easy and extendable use the following behavior.

For your validation rules you will need a few new methods like validateIdentical (to ensure the password and the confirmation password match) as well some rules on min length etc (more on this and some code in the next section as well as the linked behavior code).

If you use passwords in multiple places (or just use password stuff in multiple apps) it might make sense to separate the code in a clean way. See the next chapter.

Introducing my Passwordable behavior

The code can be found on github (CakePHP2.0) [1.3 - careful, different name back then]. What it does is taking care of all the stuff that usually is done redundant in several places: hashing, validating, whitelisting, ensuring fields, … Using the behavior the controller code stays slim (remember: fat model, slim controller). Especially for beginners this really helps to keep it clean and safe. But I use is everywhere to keep it DRY. In case I have to adjust I usually only have to touch the behavior code.

The most important fact the behavior automatically takes care of is the update process. If you have the password field in your form and you don’t enter anything it will just skip the password changing part. As soon as you enter something the behavior assumes that you actually want to change it then. Usually this has to be covered in the controller or model at some point. This is also the point most beginners struggle with. The overhead is reduced to the behavior as responsible element.

You can override the default values with Configure::write('Passwordable.auth', 'MyAuth'); for example (config/bootstrap). It is also possible to change them at runtime – where you include the behavior. It is important NOT to globally assign the behavior to the model – for security purposes. Only use it in those actions that actually work with the password (update/reset/…). This way other actions cannot accidently use the behavior to update the password (or by tempering with the forms).

PWD_MIN_LENGTH and PWD_MAX_LENGTH are constants and can be set in the bootstrap.

Register forms

The basic use case first. We just attach the behavior to the model and and the two password fields in the view. It boils down to:

if ($this->request->is('post') || $this->request->is('put')) {
    $this->User->Behaviors->attach('Tools.Passwordable');
    if ($this->User->save($this->request->data, true, array('pwd', 'pwd_repeat', ...))) {
        //SUCCESS flash/redirect
    }
    //ERROR flash
    unset($this->request->data['User']['pwd']);
    unset($this->request->data['User']['pwd_repeat']);
}

The register form then contains

echo $this->Form->input('pwd', array('type'=>'password', 'value'=>'', 'autocomplete'=>'off'));
echo $this->Form->input('pwd_repeat', array('type'=>'password', 'value'=>'', 'autocomplete'=>'off'));

Admin Forms

if ($this->request->is('post') || $this->request->is('put')) {
    $this->User->Behaviors->attach('Tools.Passwordable', array('allowEmpty' => true, 'confirm' => false));            
    if ($this->User->save($this->request->data)) {
        //YEAH
    } else {
        //OH NO
    }
}

Using allowEmpty here is quite important. This makes the validation skip the password fields if you do not enter anything. The password will be left alone. As admin I also might not want to retype the password. So confirm can be set to false (although a confirmation field is always a good idea).

/users/edit view

Here is the most common scenario of a complete edit form for the user with only basic password input:

if ($this->request->is('post') || $this->request->is('put')) {
    $this->User->Behaviors->attach('Tools.Passwordable', array('allowEmpty' => true));          
    if ($this->User->save($this->request->data)) {
        //YEAH
    } else {
        //OH NO
    }
}

You have your two input fields in your form again which can be left empty to not update the password. Simple, isn’t it?

On an edit form the user should not have to be forced to change his password. Leaving the field empty will just skip that part. As soon as the user enters a new password, though, validation will get triggered for this field as specified in your settings.

If you use whitelisting (which is a good idea) please make sure, that you set the whitelist to only those fields from your actual model that are allowed to be updated (or use Security component). Adding pwd and pwd_repeat etc should not be necessary as the behavior takes care of adding them to the whitelist.

Separate password form for the user

Note: This is the form with only the password to be changed – see the other form above for a complete edit view

if ($this->request->is('post') || $this->request->is('put')) {
    // attach the behavior and force the user to enter the current password for security purposes
    $this->User->Behaviors->attach('Tools.Passwordable', array('current'=>true));
    $this->request->data['User']['id'] = $this->Session->read('Auth.User.id');
    if ($this->User->save($this->request->data, true, array('pwd', 'pwd_repeat', 'id'))) {
        //SUCCESS flash/redirect
    }
    //ERROR flash
 
    // pw should not be passed to the view again for security reasons
    unset($this->request->data['User']['pwd']);
    unset($this->request->data['User']['pwd_repeat']);
}

As user I have to first provide the old password and confirm (retype) the new password. Confirmation is active by default. The advantage of such a separate view might be that you can reduce complexity for the user. Especially with confirmation of the current password or more.

Login

This is only for completeness. The login part matches the one in the documentation. Here we use the field that matches the one in the database – so password in most cases:

echo $this->Form->input('password');

On important issue, though: For the login forms some forget to clear the password after an unsuccessful post.

if ($this->request->is('post') || $this->request->is('put')) {
    if ($this->Auth->login()) { # for 1.3 use ->user() here
     ...
    }
    // the important part after every post:
    $this->request->data['User']['password'] = '';
}

Now the password field stays empty. Which is usually a good thing to do in forms. The Passwordable behavior is not needed here as the Auth component already takes care of the login procedure. That’s why “password” as fieldname is all right in this case.

Is there more?

Yes, there is. You can take a look at the behavior itself for more configuration or peek into its test case. ‘allowSame’ might be interesting. false requires the user to use a different password then the one previously entered. This can be useful on a separate “change password” page and if you want to force the user to change it to something than it was before.

Your own validation rules

If you do not provide any validation rules for your password fields, the behavior will automatically use the “best practice” ones of his own. But you can always define your own ones in the model to overwrite then (not recommended, though).

Notes

In Cake1.3 the framework automatically hashes the field “password”. that’s one of the reasons why you should use “pwd” or something similar and either let the behavior/model handle it or assign “pwd” to “password” manually on save. In Cake2.0 no automatic hashing will be done anymore. But it will still be useful not to use “password”. Especially because the validation rules would not apply to a sha1 string etc^^

For resetting local passwords for development purposes (or after salt/hash changes) you might want to look into my password reset shell script.

And if you want to create your first (admin) user, you might want to take a look at my User shell which saves you the trouble of manually hashing a password and running some custom sql query. just hit cake Tools.User and “bake yourself one”.

Note: this is written for 2.x (so $this->request->data instead of this->data is being used inside the controller scope).

Working with passwords in CakePHP
0 votes, 0.00 avg. rating (0% score)
 
20 Comments

Posted by Mark in CakePHP

 

Tags: , , , , , ,

Leave a Reply

Tip:
If you need to post a piece of code use {code type=php}...{/code}.
Allowed types are "php", "mysql", "html", "js", "css".

Please do not escape your post (leave all ", <, > and & as they are!). If you have encoded characters and need to reverse ("decode") it, you can do that here!
 

 
  1. Steve

    February 5, 2012 at 22:51

    I'm trying to use the behavior with a registration form and have a problem. If the entered pwd is shorter than the minimum length I get the following:

    Notice (8): Undefined offset: 2 in [C:\wamp\www\cake_1_3\cake\libs\view\helpers\form.php, line 487

    Things work as expected for valid password lengths, and when the pwd_repeat doesn't match.

    I'm using cakephp 1.3.

    Any ideas what would cause this?

     
  2. Mark

    February 6, 2012 at 01:39

    Maybe it has to do with the validation rules?
    I only use the 2.x version nowadays. But it must be sth related to this.

     
  3. Steve

    February 6, 2012 at 19:00

    The problem turned out to be with the array used in the behavior's validation message parameters.

    When I change the following:

    'message' =&gt; array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),

    to:

    'message' =&gt; 'some message…',

    things worked fine.

    Any examples of how I would use the behavior in a lost password scenario?

     
  4. Mark

    February 7, 2012 at 00:03

    oh, thats possible
    This has been introduced in cake2.x I think. But since 1.3 I used my own custom invalidate() method which can also handle it quite well (AppModel):

    /**
    	 * Overrides the Core invalidate function from the Model class
    	 * with the addition to use internationalization (I18n and L10n)
    	 * @param string $field Name of the table column
    	 * @param mixed $value The message or value which should be returned
    	 * @param bool $translate If translation should be done here
    	 */
    	function invalidate($field, $value = null, $translate = true) {
    		if (!is_array($this->validationErrors)) {
    			$this->validationErrors = array();
    		}
    		if (empty($value)) {
    			$value = true;
    		} else {
    			$value = (array)$value;
    		}
     
    		if (is_array($value)) {
    			$value[0] = $translate ? __($value[0], true) : $value[0];
     
    			$args = array_slice($value, 1);
    			$value = vsprintf($value[0], $args);
    		}
    		$this->validationErrors[$field] = $value;
    	}

    For "lost password scenarios" you first need to find the record to the given email/username and send an activation email with a link to a page where he can enter a new password.
    there it will be the same again as above: pwd + pwd_repeat must match.
    That's all.

     
  5. Steve

    February 7, 2012 at 00:39

    One other problem. Now that I've replaced the 'password' field with the 'pwd' field in my registration form the auto login after successful registration no longer works.

    I do the following (if the User-&gt;save is successful):

    $this-&gt;Auth-&gt;login($this-&gt;data);

    Since a 'password' field is no longer on the form this doesn't work.

    I've tried inserting the following before the Auth-&gt;login:

    $this-&gt;Auth-&gt;fields = array(
    'username' =&gt; 'email',
    'password' =&gt; 'pwd'
    );

    (I use email in place of username) but to no avail. I also tried passing an array of login credentials to auth-&gt;login but that didn't work either.

    With this 'pwd' approach is there any way to follow a successful registration with an auto login?

     
  6. Mark

    February 7, 2012 at 00:48

    My guess:
    Either use "password" instead "pwd" for the field (you can pass on this setting to the behavior)
    or use some beforeValidate/beforeSave() method in the model to copy the pwd field content to password.
    It should be already hashed, though!
    By the way:
    in cake2 you can always call Auth->login() manually. Not sure how to get around that in 1.3.

     
  7. Steve

    February 7, 2012 at 01:54

    In your comment – 'It should be already hashed, though!' pointed the way. I was attempting the auto login with the unhashed value of 'pwd'.

    So now if the User save is successful I do the following:

    $this->data['User']['password'] =   $this->Auth->password($this->data['User']['pwd']); 
    $this->Auth->login($this->data);

    And it works!

    Thanks for the help (and the behavior)

     
  8. Yien

    April 28, 2012 at 06:59

    Hello, I am using your change password behaviour

    Why is the Security::hash method return 2 different kind of output.

    ie:
    I use these line

    $text = Security::hash('arandompwd', null, true);
    echo $text;

    will output a correct hashed password.

    but using your behaviour in the DB is another version of hashed password..the users cannot authenticate with the new password ?

     
  9. Mark

    April 28, 2012 at 11:40

    Yien, if you don't change the defaults, the behavior will use the exact same method (with the same arguments). therefore the password should be correctly hashed.

    where are you using your own hash method – at what part of the code? also – did you change anything there in your core? because the auth component then might use those different settings for the login mechanism and will therefore fail.

     
  10. Yien

    April 28, 2012 at 17:41

    Dear Mark,

    Thanks for your reply. After a few tests, found out that password is hashed twice.

    1. I have a beforeSave() function in User Model, which is following the default Cake 2.0 ACL tutorial.

    public function beforeSave() {
    if (isset($this-&gt;data[$this-&gt;alias]['password'])) {
    $this-&gt;data[$this-&gt;alias]['password'] = AuthComponent::password($this-&gt;data[$this-&gt;alias]['password']);
    }
    return true;
    }

    If follow your naming practice(using login form, changing the 'password' field to custom name), then set the beforeSave() to detect it(like your tut above), the auth won't login user.

    2. The beforeSave() function I use in #1, will again hash the password, along with your plugin changepwd behaviour used.

    3. I had to use this line in beforeSave()

    if(!empty($this-&gt;data['User']['password']) &amp;&amp; !isset($this-&gt;data['User']['id']))

    So that 'ChangePassword' action is not counted in, only new register or add user manually will trigger this auto hash.

    I then use a manual Security::hash method in the "forgot password" action, when a new passwd is generated randomly.

    This is definitely not the best solution, I have to live with it at the moment.

     
  11. Noob

    July 3, 2012 at 14:50

    I renamed the field in the form from 'password' to 'pwd'. I used your beforeSave() -code, but it doesn't work.

    Should I rename the field in the DB as well?

     
  12. Mark

    July 4, 2012 at 13:03

    you are not supposed to use the before save with the behavior. as you can see above the behavior is introduced after and replaces this altogether :)

     
  13. Dan Horrocks

    July 26, 2012 at 12:48

    Hey,

    I'm trying to use your behavior and it's working well, I've just got one problem but I'm unsure if it's intended or if I'm doing something wrong!

    When I'm editing a user, I've set allowEmpty to true so they can leave the pwd field blank if they want to keep the current password, this works fine.

    But when I change a user's password the validation on the length is no longer applied, I can see the code which does this but can't seem to work out how to make it work in the way I described. (Must be between x and y only when there's not a blank password!)

    # allowEmpty?
    		if (!empty($this->settings[$Model->alias]['allowEmpty'])) {
    			$Model->validate[$formField]['between']['rule'][1] = 0;
    		}
     
  14. Mark

    July 26, 2012 at 13:14

    I really should clarify this – let me rewrite the tutorial later on to make it clearer. also I have to recheck if it works with the latest version 2.3 of cake.

     
  15. Dan Horrocks

    July 26, 2012 at 13:27

    Ah I see, thanks for the heads up there, could have been rather embarrassing later down the line.

    I seem to be having problems though, For example I'm editing a user and have placed your behaviour into my app and am using the code:

    {code type=php}if ($this-&gt;request-&gt;is('post') || $this-&gt;request-&gt;is('put')) {
    $this-&gt;User-&gt;Behaviors-&gt;attach('ChangePassword');

    {/php}

    This works fine when entering a new password for the user (Between 8 and 16 chars and confirms with pwd_repeat)

    But doesn't seem to work when I leave the password field blank so to not change the user's password, it still pops up saying the password should be between 8 and 16 chars.

    Many thanks for your help, I'm just pretty stumped!

     
  16. Mark

    July 26, 2012 at 14:40

    I updated the above tutorial. The issue was mainly with `allowEmpty`=>true.
    If you don't set this to true the user will be required to enter his credentials. This is the default use case for registration and password changing sites.

    But for users edit views you expect it to skip the field if you do not enter any new password.
    Therefore you need to set `allowEmpty` to true in this case telling the behavior that we do not necessarily require a password change here.

    What you expected should then be covered without any additional configuration, by the way. It automatically leaves the password field in the database alone as long as you don't enter anything in the input field.

     
  17. Frank

    March 3, 2013 at 09:47

    Hi Mark

    If use "current" / "pwd_current" I get this error message:

    Fatal Error
    Error: Call to undefined method Component::identify()	
    File: C:\xampp\htdocs\ebsst\Model\Behavior\PasswordableBehavior.php	
    Line: 145
     
    Notice: If you want to customize this error message, create ebsst\View\Errors\fatal_error.ctp

    I use this behaviour standalone (without tools).

    Can you help?

    Thanks,
    Frank

     
  18. Mark

    March 3, 2013 at 12:17

    Using it standlone is not recommended. But nevertheless it should still work in this case :)
    Indeed, without setting "auth" to "Auth" manually in the settings it would run into this issue. I fixed it. Check out the master branch.
    And thank you for reporting it.

     
  19. Frank

    March 3, 2013 at 13:25

    Hi Mark

    Thanks for fixing this!

    Frank

     
  20. Frank

    March 4, 2013 at 16:27

    Hi Mark

    I've replaced the previous version with your modified one from the master branch. It seems the behaviour is completly broken. Validation of the password fields is skipped. The value from the password-field will be directly saved. Ive not looked any deeper yet – perhaps you know already why this happens… ;)

    Thanks,
    Frank