Thursday, March 7, 2013


Forcing a Password Change

I looked around at a few methods for forcing users to change their password. Typically you may allocate a user an initial  generated password however users will often prefer to have a password they are able to remember. This post will run through allocating a user a generated password and then forcing the user to change the password at their first login. I could not find an easy to follow method all in one place explaining the methodology so I thought I would try to bring the whole process together in one place.

First we will create an initial password for the new user and there are a number of ways to achieve this however I will show you a simple way to create an initial password. In your user controller when creating a new user the following function is simple and easy to implement. Let us assume we are creating a new user that needs a password automatically generated. Create a function in your controller.

public function createPassword()
{
$chars = "abcdefghjkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
$str="";
$size = strlen( $chars );
for( $i = 0; $i < 9; $i++ ) {
$str .= $chars[ rand( 0, $size - 1 ) ];
}
return $str;
}

This function will create a password 9 characters in length. I like to omit ambiguous characters such as lowercase letter 'l', uppercase 'I', and 'O' because there can be confusion if the password has a zero or a letter 'O' and he same with the number '1' and the lowercase letter 'l'. You could assign special characters to the $chars variable depending on the password complexity required. To call this function in the user controller all you need to do is use '$this->createPassword to create a random password. In this example I encrypt the password using a simple Secure Hash Algorithm,

$client_password = $this->createPassword();
$new_client = new Client;
$new_client->real_name = 'Joe';
$new_client->username = 'cleric';
$new_client->password = sha1($client_password);
$new_client->active = 1;
$new_client->save();

Now we need some way to identify the user logging in for the first time. I track every time a user logs into a web application so I have a field called "last_login" in the user table. When creating the user leave the last_login field as null so when a user logs into the web application, if the last_login field is NULL then force the password change.

When I create a web application I usually write it in such a way that a user must be logged in to access any page within the application. I will not run through how to do this however look at the article written by Larry Ullman - Forcing Login for All Pages in Yii.  The benefit to using Ullman's approach is that just a small amount of code will add authorisation to the whole web site and you can re-use the method in other applications as the implementation is very straight forward.

Now in the SiteController in the function actionLogin test to see if the last_login field is null. If that is the case then redirect the user to allow the user to change their password to something easier to remember. Do a simple query to check the last_login for the user in the actionLogin method in your SiteController.   For example:
public funcion actionLogin()
{
.........

//user login for first time

$upduser=User::model()->findByPk($userId);

if ($upduser->last_login == '') {
$this->redirect('index.php?r=user/firstlogin');
}

else // client has logged in previously
{
$upduser=User::model()->findByPk($id]);
$upduser->last_login = new CDbExpression('NOW()'); //update the users last_login
$upduser->save();
$this->redirect('index.php?r=user/welcome');
}
}

If the last_login field is null the user will get redirected to the UserController::firstlogin method. The methodology I use is adapted from a good StackOverflow discussion which you can find at Yii, best way to implement “user change of password”.

In your UserController create a function called actionFirstLogin: An example:

//function for clients to allow them to change their password on the first login
public function actionFirstlogin()
{
$model=new ChangePasswordForm();
            if (isset($_POST['ChangePasswordForm'])) {
        $model->setAttributes($_POST['ChangePasswordForm']);
        if ($model->validate()) {
            $model->saveNewPassword();
                        $this->redirect('index.php?r=client/postaldata'); //or wherever you want to redirect the user
        }
    }
     $this->render('updatefirst', array('model'=>$model));
}
Create a CFormModel rather than an a CActiveRecord called ChangePasswordForm. This class goes into the directory containing your models. The code for this is as follows remembering that the file must be called ChangePasswordForm.php.

?php
// models/ChangePasswordForm.php

class ChangePasswordForm extends CFormModel
{
    /**
     * @var string
     */
    public $currentPassword;

    /**
     * @var string
     */
    public $newPassword;

    /**
     * @var string
     */
    public $newPasswordRepeat;

    /**
     * Validation rules for this form.
     *
     * @return array
     */
    public function rules()
    {
        return array(
            array('currentPassword, newPassword, newPasswordRepeat', 'required'),
            array('currentPassword', 'validateCurrentPassword', 'message'=>'This is not your password.'),
            array('newPassword', 'compare', 'compareAttribute'=>'newPasswordRepeat'),
            array('newPassword', 'match', 'pattern'=>'/^[a-z0-9_\-]{8,}/i', 'message'=>'Your password be 8 characters (letters/numbers) in length.'),
        );
    }

    /**
     *
     * @return string Hashed password
     */
    protected function createPasswordHash($password)
    {                                                                                                                                                                
        return sha1($password);
    }

    /**
     *
     * @return string
     */
    protected function getUserPassword()
    {
        $curr_user = Yii::app()->db->createCommand()
                    ->select('id, password')
                    ->from('user')
                    ->where('username=:uid', array(':uid'=>Yii::app()->user->uname)) //using a session variable
                    ->queryRow();
        return $curr_user['password'];
    }

    /**
     * Saves the new password.
     */
    public function saveNewPassword()
    {
        $user=User::model()->findByAttributes(array('username'=>Yii::app()->user->uname));
        $user->password = $this->createPasswordHash($this->newPassword);
        $user->last_login = new CDbExpression('NOW()');
        $user->update();
    }

    /**
     * Validates current password.
     *
     * @return bool Is password valid
     */
    public function validateCurrentPassword()
    {
        return $this->createPasswordHash($this->currentPassword) == $this->getUserPassword();
    }
}
The pattern you use in this FormModel depends on the password complexity you require for your site - in this example it is a simple pattern - 'pattern'=>'/^[a-z0-9_\-]{8,}/i', 'message'=>'Your.............
Remember I am just using a simple SHA hash to store my passwords in this example.

You will now need to create a view that will take 3 inputs - Current Password, New Password, Repeat New Password. This view in this example is called updatefirst.php and the view is in the User view directory. The code for this view to allow the input of the old and new password will need to be modified to your needs. An example of the updatefirst.php view file code:
NOTE: Again there are some session variables in this code Yii::app()->user->title) and I am fond of the bootstrap extension hence the bootstrap widgets. Finally I am using renderPartial so you will need 2 view files


<h4>Enter new password for <?php echo Yii::app()->user->title; ?></h4>
<?php $this->widget('bootstrap.widgets.TbLabel', array(
     'type'=>'info', // 'success', 'warning', 'important', 'info' or 'inverse'
     'label'=>'You must change your password - Should be 8 characters long (mixture of letters and numbers',
));

echo '<br /><br />';
$this->renderPartial('_updateformfirst', array('model'=>$model)); ?>

=========================================================
Now the code for the _updateformfirst.php file

<?php
/* @var $this UserController */
/* @var $model User */
/* @var $form CActiveForm */
?>

<div class="wide form">

<?php $form=$this->beginWidget('CActiveForm', array(
'id'=>'user-form',
'enableAjaxValidation'=>false,
)); ?>

<p class="note">Fields with <span class="required">*</span> are required.</p>

<?php echo $form->errorSummary($model); ?>

<div class="row">
<?php echo "Enter your current password <span style = 'color:red'><sup>*</sup></span>"; ?>
<?php echo $form->passwordField($model,'currentPassword',array('class'=>'span2','maxlength'=>20)); ?>
</div>

<div class="row">
<?php echo "Enter your new password <span style = 'color:red'><sup>*</sup></span>".'&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'; ?>
<?php echo $form->passwordField($model,'newPassword',array('class'=>'span2','maxlength'=>20)); ?>
</div>

<div class="row">
<?php echo "Repeat your new password <span style = 'color:red'><sup>*</sup></span>".'&nbsp;&nbsp;'; ?>
<?php echo $form->passwordField($model,'newPasswordRepeat',array('class'=>'span2','maxlength'=>20)); ?>
</div>


<div class="row buttons">
<?php echo CHtml::submitButton('Update Password'); ?>
</div>

<?php $this->endWidget(); ?>

</div><!-- form -->

Now if the user enters the old password correctly and the new password matches the repeat new password and the new password meets the password complexity defined the user will have successfully changed their password and the user will get redirected to the page you code into the UserController method actionFirstLogin.

While there are a few steps to creating this workflow for a password change it is fairly easy to implement if you follow the steps outlined. For more information about changing passwords (such as forcing password changes on a time basis e.g. every month) take a look at :

  1. Model password confirmation field
  2. Force a User to Change Their Password (ChangePasswordFilter)
  3. Change Password Action Required - Ideas?
  4. Various Yii Forum Search Results
With some patience and logic it is not difficult to add this feature into your Yii Application.