Showing posts with label passwords. Show all posts
Showing posts with label passwords. Show all posts

Monday, August 8, 2011

Reset Forgotten Password

For a secure application one issue that will arise repeatedly is users forgetting their passwords. One method of correcting this is to allow users to reset their passwords by submitting their email address. Since the new password will be only be sent to a valid email address, only the appropriate user will know the new password.

For applications open to the public this method is not advised since anyone could possibly find a valid user email address and spam them with reset passwords.

First create the form /app/views/users/forgot.ctp
<h2>Forgot Password</h2>
<p>Submit your email address and a new password will be emailed to you.</p>
<?php
    echo $form->create('User');
    echo $form->input('email');
    echo $form->end('Send Password');
?>
<script type="text/javascript">
document.getElementById('UserEmail').focus()
</script>
Next edit your Users controller at /app/controllers/users_controller.php and add the forgot function.
    function forgot(){
        if(!empty($this->data)){
            $user = $this->User->findByEmail($this->data['User']['email']);
            if($user){
                $user['User']['password'] = $this->__generatePassword();
                // Since my users model requires a confirm_password to match the password on update, it is required here
                // See http://drug-ed.blogspot.com/2011/08/authentication-and-passwords.html
                $user['User']['confirm_password'] = $user['User']['password'];
                if ($this->__sendEmail($user['User']['email'], 'New Password', $user['User'], 'newpass')) {
                    if ($this->User->save($user['User'], array('fieldList' => array('password')))) {
                        $this->Session->setFlash(__('An email has been sent with your new password.', true));
                        $this->redirect(array('action' => 'login'));
                    } else {
                        $this->Session->setFlash(__('The password cound not be saved. Please try again.', true));
                    }
                } else {
                    $this->Session->setFlash(__('The email could not be sent. Please try again.', true));
                }
            } else {
                $this->Session->setFlash('User could not be found.');
            }
        }
    }
My __sendEmail() function above is described in detail here. Here is my __generatePassword function. Feel free to use it or create your own.
    function __generatePassword($length = 8){
        $characters = 'abcdefghijklmnpqrstuvwxyz';
        $numbers = '123456789'; // No 0 or O so as not to confuse
        $more = '!@#$%^&_+=-';
        $password = '';
        $alt = time();
        for($i = 0; $i <= $length; $i++){
            $alt += rand() % 10;
            if($alt % 3 == 1){
                if($alt % 2 == 1){
                    $password .= strtoupper($characters[(rand() % strlen($characters))]);
                } else {
                    $password .= $characters[(rand() % strlen($characters))];
                }
            } elseif($alt % 3 == 2){
                $password .= $numbers[(rand() % strlen($numbers))];       
            } else {
                $password .= $more[(rand() % strlen($more))];       
            }
        }
        return $password;
    }
Lastly, create your email templates. They can be quite simple. /app/views/elements/email/text/newpass.ctp
A new password has been requested for the account: <?php echo $data['username'].'.'.PHP_EOL.PHP_EOL; ?>
Your new password is: <?php echo $data['password'].PHP_EOL.PHP_EOL; ?>
Please use this password to log in, then you can change your password by clicking "Change Password."
Since the email is sent before saving, the password isn't hashed yet and will be visible to the user.

Lastly, ensure that you allow access to the /users/forgot view by editing /app/app_controller.php and add 'forgot' to your $this->Auth->allow() line.

Tuesday, August 2, 2011

Authentication and Passwords

One issue that will pop up is trying to add or edit users with Authentication enabled. Validation won't work properly on the password field because Authentication will hash the password BEFORE it attempts to validate. For example checking for a minimum length will always succeed regardless of the actual password because SHA1 hashed passwords will always be 40 characters.

One method around this is performing the hashing manually. To do this you have to tell your users controller you want to perform your own hashing. Edit /app/controllers/users_controller.php and add the following function:
    function beforeFilter(){
        parent::beforeFilter();
        if($this->action == 'add' || $this->action == 'edit' || $this->action == 'password'){
            $this->Auth->authenticate = $this->User;
        }
    }
Now when you are using the add or edit actions authentication is done manually. You'll see why the action password is in there later.
Next edit the users model /app/model/users.php and add the following functions:
    function hashPasswords($data, $enforce=false) {
        if($enforce && isset($this->data[$this->alias]['password'])) {
            if(!empty($this->data[$this->alias]['password'])) {
                $this->data[$this->alias]['password'] = Security::hash($this->data[$this->alias]['password'], null, true);
            }
        }
        return $data;
    }

    function beforeSave() {
        $this->hashPasswords(null, true);
        return true;
    }
Now your users model will hash the passwords before save, allowing validation to take place first.

Another problem, however, is when editing a user, the hashed password is used in the password field of the form and becomes hashed again on save, actually changing the password! I'm surprised this issue isn't addressed in the core of CakePHP.

One way around this is to modify your edit view to clear the password field of the hashed password. Edit /app/views/users/edit.ctp and change this
    echo $this->Form->input('password');
to this
    echo $this->Form->input('password', array('value' => ''));
This will require you to enter a password every time you edit the user because your validation is set to require 5 characters in the password field.

So to get around that simply remove the password field from your edit form. You can also remove the edit action from your beforeFilter function in the users controller. Now you can edit the user without worrying about the password.

Then, to edit the password create a password function in your controller and a separate view. Edit /app/controllers/users_controller.php and add this function:
    function password($id = null){
        /* Only edit own account unless admin */
        if(!$this->Auth->user('admin')){
            $id = $this->Auth->user('id');
        }    
        if (!$id && empty($this->data)) {
            $this->Session->setFlash(__('Invalid user', true));
            $this->redirect(array('action' => 'index'));
        }
        if (!empty($this->data)) {
            if ($this->User->save($this->data, array('fieldList' => array('password')))) {
                $this->Session->setFlash(__('The password has been saved', true));
                $this->redirect(array('action' => 'index'));
            } else {
                $this->Session->setFlash(__('The password could not be saved. Please, try again.', true));
            }
        }
        if (empty($this->data)) {
            $this->data = $this->User->read(null, $id);
            /* Don't display current hashed password */
            $this->data['User']['password'] = '';
        }
Now create /app/views/users/password.ctp
<div class="users form"> 
<?php echo $this->Form->create('User'); ?>
    <fieldset><legend>Change Password</legend>
    <?php
        echo $this->Form->hidden('id');
        echo $this->Form->input('password', array('label' => 'New Password'));
        echo $this->Form->input('confirm_password', array('type' => 'password'));
    ?>
    </fieldset>
    <?php echo $this->Form->end('Save Password'); ?>
</div>
Then modify your users model /app/models/user.php and add the following validation rules and functions:
        'password' => array(
            'Your password must be at least 5 characters' => array(
                'rule' => array('minlength', 5)
            ),
            'You must enter the same password twice' => array(
                'rule' => array('matchPasswords', 'confirm_password'),
                'on' => 'update'
            )
        ),
        'confirm_password' => array(
            'rule' => 'notEmpty'
        )

    function matchPasswords($data, $confirm_password){
        if ($data['password'] != $this->data[$this->alias][$confirm_password]) {
            $this->invalidate($confirm_password, 'You must enter the same password twice');
            return false;
        }
        return true;
    }
Set appropriate authentication as desired, upload the files and you should be all set.