CakePHP 1.3’s Auth Component has made authentication and ACL much easier. It however, leaves most the implementation of user management to the developer. Here are some helpful lessons we have learned to make this process less painful.

(1) To defer hashing of the password until the new or updated record is about to be saved so that we can do validation on that password:

(a) Set the Auth component’s authenticate variable to be the User model for add and edit actions. This will cause the Auth component’s hashPassword’s method to defer to the hashPassword method in the User model, if such a method is present. You add the following lines to the beforeFilter:

       // Use the User model's password hashing function for add and edit
       $actions = array('add', 'edit', 'admin_add', 'admin_edit');
       if (in_array($this->action, $actions)) {
           $this->Auth->authenticate = $this->User;
       }

(b) In the User.hashPasswords method, take two arguments. The first, which is all that will be passed in by Auth.hashPasswords, is the data array. The second is a flag that should default to false. This flag toggles whether you will actually hash your passwords or not. In operation when called from Auth controller (all through add and edit operations), the passwords are not hashed so that we can still work with them in plain text for things like validation.

This idea comes from:
http://nuts-and-bolts-of-cakephp.com/2008/10/08/demystifying-auth-features-in-cakephp-12/

   function hashPasswords($data, $do_it = false) {

       // Don't change the password if the form data password is blank
       if ( isset($data[$this->alias]['password']) &&
            empty($data[$this->alias]['password']) ) {
           unset($data[$this->alias]['password']);
       }

       // Only just before saving, hash the passwords
       if ($do_it) {
           if ( isset($data[$this->alias]['password']) &&
                ! empty($data[$this->alias]['password']) ) {

               // Watch this hashing operation.  If the password() function
               // in Auth component changes for any reason, this line
               // will need to change accordingly.
               $data[$this->alias]['password'] =
                   Security::hash($data[$this->alias]['password'], null, true);
           }
       }

       return $data;
   }

(c) Then, to hash the passwords at the last minute before you save the record, you make a call from the User model beforeSave callback that supplies True to the User.hashPasswords method.

   // beforeSave hashes the passwords for create and update operations
   function beforeSave() {
       $this->data = $this->hashPasswords($this->data, true);
       return true;
   }

(2) To stop edit operations from overwriting or double encrypting the password when it is not being modified in the edit operation:

(a) When you are loading data before an edit operation, clear the password from the data. This way, the user will not be submitting the form with the current hashed password as his new password. The result is this, with the *’d lines being new:

       if (empty($this->data)) {
           $this->data = $this->User->read(null, $id);
*            // Display an empty password box
*            $this->data['User']['password'] = null;
       }

(b) Then, as you’ve already seen in the User.hashPasswords code above, if the password is an empty string, we unset that array index so that it will not effect the existing data when the record is saved. This only makes sense, by the way, in the context of doing our own hashing with the User.hashPasswords method. Otherwise, the password would already be hashed by the time we tried to check it if was empty.

[ From User.hashPasswords ]

       // Don't change the password if the form data password is blank
       if ( isset($data[$this->alias]['password']) &&
            empty($data[$this->alias]['password']) ) {
           unset($data[$this->alias]['password']);
       }

(3) And finally, now that we have a plaintext password at User validation time, we can validate the characteristics of that password and add a password_confirmation field in a straightforward manner.

(a) Add a password_confirmation field to User add and edit forms:

       echo $this->Form->input('password');
       echo $this->Form->input('password_confirmation',
                           array('type' => 'password'));

(b) Add your password validations to the User model, including a custom validation for comparing the password and the password_confirmation:

   var $validate = array(

       [...]

       'password' => array(
           'notEmpty' => array(
               'rule' => array('notEmpty'),
               //'message' => 'Your custom message here',
               'allowEmpty' => false,
               'required' => true,
               //'last' => false, // Stop validation after this rule
               'on' => 'create', // Limit validation to 'create' or 'update' operations
           ),
           'minLength' => array(
               'rule' => array('minLength', 4),
               'message' => "Passwords must be at least 4 characters long",
               'allowEmpty' => true,
           ),
           'confirmPassword' => array(
               'rule' => array('confirmPassword', 'password_confirmation'),
               'message' => "Password and Password Confirmation must match",
           ),
       ),
       'password_confirmation' => array(
           'notEmpty' => array(
               'rule' => array('notEmpty'),
               'message' => 'Password and Password Confirmation must be supplied when setting the password',
               'allowEmpty' => false,
               'on' => 'create',
           ),
       ),

       [...]
 );

(c) Add the password_confirmation method, as specified in the ‘confirmPassword’ validation rule for password, to the User model.

This idea comes from:
http://bakery.cakephp.org/articles/aranworld/2008/01/14/using-equalto-validation-to-compare-two-form-fields

   // Validate the password & password_confirmation fields
   function confirmPassword($field = array(), $confirm_field = null) {
       foreach ($field as $key => $value) {
           // compare the field being validated with the confirmation field
           if ($value !== $this->data[$this->alias][$confirm_field]) {
               return false;
           }
       }

       // If no inequality caused us to return false, the validation passes
       return true;
   }

Thanks to Vin Marshall who provided this article after discovering this issue in the CakePHP Auth Component.