Best way to implement "implicit change of password"? [SOLVED]

I would like to add a “change password” feature to the user management area. What I would like to see is that if the user is edited (that is, the user’s attributes like first_name, last_name are displayed - as are the two fields password and password_repeat), the admin can not only change some of the user’s attributes but also can set the user’s password in one go.

So, the form would look like this:




First name: __________

Last name:  __________

Password:          _____________

Password (repeat): _____________

[Update User]



The application should behave differently depending on whether the password fields are filled or not:

(1) if the password fields are both empty, only the remaining attributes should be updated

(2) if the password fields are not empty, they should be checked (must be equal) and saved along with the other attributes

How can this be achieved in a clear and concise way?

I would assume the distinction between case (1) and case (2) could be solved with scenarios? So in the UserController.php, can I use something like the following snippet?




public function actionUpdate($id)

{

  [...]


  if(isset($_POST['User']))

  {

    $model->attributes=$_POST['User'];


    // LOOK HERE! Change scenario:

    if ($model->password!=='')

      $model->setScenario('changePassword');


    if($model->save())

      $this->redirect(array('view','id'=>$model->user_id));

  }


  [...]

}

Anyway, at the moment I don’t see any possibility to exclude the attribute “password” from being saved if the corresponding input field has not been filled. How would I be able to achieve this? Any clue appreciated …

Yea that’s the right idea, but you need two password fields that are not part of the database to work it out. So what you need to do is add two fields to your model:


<?php

class User extends CActiveRecord

{

    public $newPassword;

    public $newPasswordRepeat;


    // rest of model code

    ...

}

Then in your “_form.php” view you want use those two fields instead of the actual password field from the database. Then when you go into your controller you can do what you were doing above… except just check your dummy fields that you placed on the form. You probably also want to validate first… that will take care of displaying the error messages on the form (such as Passwords don’t match):


// First we need to validate

if ($model->validate()) // Put newPassword and newPasswordRepeat to have a compare validator (must be equal)

{

    // Will only be filled by user entered data.. real password is hidden

    // (nice because real passwords should be at least 128 chars.. we don't want to show user that)

    if ($model->newPassword !== '')

    {

        $model->password=whateverEncryptionYouUse($newPassword, ...);

    }


    if ($model->save(false)) // use false because we already validated

    ...

}

Brilliant!

Employing your hints, it was a snap to do. For other interested readers, this is the whole story (I hope I did not forget something):

(1) Enrich the model class "User" with two password fields (which are not database fields):


class User extends CActiveRecord

{

  public $new_password;

  public $new_password_repeat;


  [...]

(2) Also in the User model class, set up validation rules for comparing the passwords in case they are entered (that is, in case the scenario is "changePassword" - or "insert" where the password has to be entered in any case):


public function rules()

{

  return array(

    array('username, salt', 'required'),

    array('username', 'length', 'max'=>45),


    // THIS IS THE NEW STUFF

    array('new_password', 'length', 'max'=>50),

    array('new_password', 'compare', 'on'=>'insert, changePassword'),

    array('new_password_repeat', 'safe'),

    array('new_password, new_password_repeat', 'required', 'on'=>'insert'),


    [...]



So, in scenario "update", the new_password will not be validated by the compare validator, but on scenario "changePassword" it will. Now we care about switching the scenario: in the update case, the scenario is "update" by default. In case the user enters something into the "new_password" field, switch the scenario to "changePassword", so that the newly defined password validator will be called by $model->validate():

Furthermore, in case of creating a new user, the new password should be required (last rule in the code above).

(3) Scenario switching:

In the UserController, the function actionUpdate() gets additional logic and looks like this:


public function actionUpdate($id)

{

  $model=$this->loadModel($id);

  if(isset($_POST['User']))

  {

    $model->attributes=$_POST['User'];


    // if a new password has been entered

    if ($model->new_password !== '') {

      // set scenario 'changePassword' in order 

      // for the compare validator to be called

      $model->setScenario('changePassword');

    }


    if ($model->validate())

    {

      if ($model->new_password !== '') {

        $model->password = $model->hashPassword($model->new_password, $model->salt);

      }

      

      // the validation has already been done, skipping it with save(false):

      if($model->save(false))

        $this->redirect(array('view','id'=>$model->user_id));

    }

  }


  $this->render('update',array(

    'model'=>$model,

  ));

}



(4) The hashPassword method could be something like (in the model file User.php):


public function hashPassword($password, $salt)

{

  return md5($salt.$password);

}



(5) And finally in the view (user/_form.php), refer to the new password fields (not the database field):




[...]


<div class="row">

  <?php echo $form->labelEx($model,'new_password'); ?>

  <?php echo $form->passwordField($model,'new_password',array('size'=>50,'maxlength'=>50)); ?>

  <?php echo $form->error($model,'new_password'); ?>

</div>


<div class="row">

  <?php echo $form->labelEx($model,'new_password_repeat'); ?>

  <?php echo $form->passwordField($model,'new_password_repeat',array('size'=>50,'maxlength'=>50)); ?>

  <?php echo $form->error($model,'new_password_repeat'); ?>

</div>


[...]



(6) Modify the case "Create new user"

So far we only took care of the update case (Controller: actionUpdate). When creating a new user, the password field would now stay empty because the encryption of the field “new_password” and assignment to the model’s password field takes only place in UserController.actionUpdate(), but not in UserController.actionCreate(). I could now duplicate some code for actionCreate(), but I decided to solve the task with a simple afterValidate routine which only does the job of encrypting/assigning in the “insert” scenario.

I am not sure if this is the most elegant way to do so, but it works. So add the following function to the User model:


public function afterValidate()

{

  parent::afterValidate();

  if ($this->getScenario() === 'insert')

    $this->password = $this->hashPassword($this->new_password, $this->salt);

}



This is it! Now if a user is created, thanks to the scenario "insert", the password is required and gets validated. In the update case, if the user enters a new password, thanks to the scenario "changePassword", the new password will be validated and saved along with all other entered data. In the update case, if the user does not enter a new password, the scenario will remain "update" and the new password is ignored - only the other entered attributes are saved.

Wonderful! Many thanks, lgoss!

That seemed to do the trick. I followed the same route, but with the difference of only changing the scenario in actionUpdate() and modifying afterValidate() as follows:


public function afterValidate()

{

    parent::afterValidate();

    if(($this->getScenario() === 'insert') || ($this->getScenario() === 'changePassword'))

    {

        //hash new password

    }

}



Thanks a lot!! Really helpful solution…

Great solution. I just implemented it for the new CRM application I am building.

I would also add that it’s probably most appropriate to do the encryption and setting of the password in beforeSave event, rather than the afterValidate as afterValidate will trigger regardless when the safe method gets called and that’s not what you want… You only want to set the password if all validation has actually passed.

Let me point out my solution, based on your previous comments.

  1. Add two new attributes to user model (models/user.php)



    // Properties for creating/updating password

    public $new_password;

    public $new_password_repeat;



  1. Add these rules in User::rules()



// Both passwords always must match, but are only required on create!

array( 'new_password, new_password_repeat', 'required', 'on' => 'create' ),

array( 'new_password', 'length', 'max' => 50 ),

array( 'new_password', 'compare' ),

// Make new_password_repeat safe. If it doesn't appear in any other rule, it's considered not safe!

array( 'new_password_repeat', 'safe' ),



  1. Create a User::beforeSave() to update the actual password



    // Password hashing stripped out in the sake of brevity <img src='http://www.yiiframework.com/forum/public/style_emoticons/default/smile.gif' class='bbc_emoticon' alt=':)' />

    protected function beforeSave()

    {

        // If new password is empty, it means it's an update with blank password,

        // so we leave it untouched. 

        // If it contains some value, then let's update the actual password

        // Remember that newPassword is required on create!

        if( ! empty( $this->new_password ) )

        {

            $this->password = $this->new_password;

        }

        return parent::beforeSave();

    }



  1. In controllers/UserController.php just define 1 scenario when creating a user

	

(...)

public function actionCreate()

{

    // Just add the name of the scenario when creating the model

    $model=new User( 'create' );

(...)



  1. Modify user/_form.php




<div class="row">

  <?php echo $form->labelEx($model,'new_password'); ?>

  <?php echo $form->passwordField($model,'new_password',array('size'=>50,'maxlength'=>50)); ?>

  <?php echo $form->error($model,'new_password'); ?>

</div>


<div class="row">

  <?php echo $form->labelEx($model,'new_password_repeat'); ?>

  <?php echo $form->passwordField($model,'new_password_repeat',array('size'=>50,'maxlength'=>50)); ?>

  <?php echo $form->error($model,'new_password_repeat'); ?>

</div>




Awesome post, works with one exception:

When a new_password has been entered and it doesn’t match new_password_repeat

Other than in the user create form, the Ajax validator doesn’t indicate that there is mismatch. When I submit the form, the page simply gets reloaded instead of redirected.

Any help would be much appreciated.

Model:





class User extends CActiveRecord

{


	public $new_password;

	public $new_password_repeat;


public function rules()

	{

	

		return array(

			

			...

			

			// scenario: default scenario = update, when password is set then scenario = changePassword

			// http://www.yiiframework.com/forum/index.php?/topic/12229-best-way-to-implement-implicit-change-of-password-solved/			

			array('new_password, new_password_repeat', 'length', 'max'=>45),

			array('new_password', 'compare', 'compareAttribute'=>'new_password_repeat', 'on'=>'changePassword'),

			array('new_password, new_password_repeat', 'safe'),


                        ...




Form:





    ...


<div class="row">

      <?php echo $form->labelEx($model,'new_password'); ?>

      <?php echo $form->passwordField($model,'new_password',array('size'=>45,'maxlength'=>45)); ?>

      <?php echo $form->error($model,'new_password'); ?>

    </div>

    

    <div class="row">

      <?php echo $form->labelEx($model,'new_password_repeat'); ?>

      <?php echo $form->passwordField($model,'new_password_repeat',array('size'=>45,'maxlength'=>45)); ?>

      <?php echo $form->error($model,'new_password_repeat'); ?>

    </div>


...




Controller:





	public function actionUpdate()

	{

		$model=$this->loadModel(Yii::app()->user->id);


		// Uncomment the following line if AJAX validation is needed

		$this->performAjaxValidation($model);


		if(isset($_POST['User']))

		{ 

			

			$model->attributes=$_POST['User'];

			

			// scenario: default scenario = update, when password is set then scenario = changePassword

			// http://www.yiiframework.com/forum/index.php?/topic/12229-best-way-to-implement-implicit-change-of-password-solved/						

			

			// if a new password has been entered

    		if ($model->new_password !== '') {

      		// set scenario 'changePassword' in order 

      		// for the compare validator to be called

      			$model->setScenario('changePassword');

    		}

			

			if ($model->validate())

			{

			  // set password to non-db field new_password after scenario has been changed to changePassword (see above)

			  if ($model->new_password !== '') {

				  $model->password = $model->encrypt($model->new_password);

			  }

			  

			  // the validation has already been done, skipping it with save(false):

			  if($model->save(false))

				$this->redirect('index.php?r=user/welcome');

			}

	  }






it’s very helpful…thank you!! :D

I used the code twocandles provided, and works perfectly! Thanks!

twocandles’s works great, thank you!

One thing though: When I go to update a user, the new_password field is already populated and forgetting this and hitting submit - easy to do - causes an error, which is a bit annoying.

Is there any way of preventing the new_password field from being populated on the User update page?

many thanks

gvanto

Hi ter,

Pls,post the error message.will be easier to identify.

Follow this tutorial -

http://www.yiiframework.com/wiki/718/change-password-with-tbactiveform