Force a User to Change Their Password (ChangePasswordFilter)

You are viewing revision #7 of this wiki article.
This version may not be up to date with the latest version.
You may want to view the differences to the latest version.

next (#8) »

Sometimes you need to force a user to change their password after x number of days. This article describes how to implement this using a filter, ChangePasswordFilter.

Why can't I use afterLogin()? Sure, you could redirect a user to a change password form after they login, but there's no mechanism to keep the user on the change password form. I.e. they could easily browse to another page.

The Password Filter Class. This class calls ChangePasswordRule->changePasswordRequired() to check the user's password freshness.

<?php

/**
 * ChangePasswordFilter class file.
 *
 * @author Matt Skelton
 * @date 27-Jun-2011
 * 
 * Determines if a user needs to change their password. 
 * A user must change their password if:
 *      User->daysSincePasswordChange() > ChangePasswordRule->passwordExpiry
 */
class ChangePasswordFilter extends CFilter
{
    private $rule;

    /**
     * Runs a check to see if the user is required to change the password. This 
     * method is called before controller actions are run.
     * @param CFilterChain $filterChain the filter chain that the filter is on.
     * @return boolean whether the filtering process should continue and the action
     * should be executed.
     */
    public function preFilter($filterChain)
    {
        $allowed = true;

        if ($this->rule->changePasswordRequired(Yii::app()->user->getModel()))
        {
            $allowed = false;
            
            Yii::app()->user->setFlash('notice', 'You must change your password before you can proceed.');
            Yii::app()->user->redirectToPasswordForm();
        }

        return $allowed;
    }

    /**
     * Builds the rule for the filter.
     * @param array $rules the list of rules as set in the controller
     */
    public function setRules($rule)
    {
        $passwordRule = new ChangePasswordRule();
        $passwordRule->passwordExpiry = $rule['days'];

        $this->rule = $passwordRule;
    }
}
?>

The Password Rule Class. This class returns a Boolean indicating if the user needs to change their password.

<?php

/**
 * ChangePasswordRule class file.
 *
 * @author Matt Skelton
 * @date 27-Jun-2011
 * 
 * This class performs the actual validation and returns a boolean indicating if
 * the user is required to change their password.
 */
class ChangePasswordRule extends CComponent
{
    public $passwordExpiry;

    /**
     * Checks if a user is required to change their password.
     * @param type $user the user whose password needs to be validated
     * @return boolean if the user must change their password
     */
    public function changePasswordRequired($user)
    {
        $passwordChangeRequired = false;

        if ($user->daysSincePasswordChange() > $this->passwordExpiry)
            $passwordChangeRequired = true;

        return $passwordChangeRequired;
    }

}
?>

Controller Class (Parent Controller - client controllers should extend this one). These two methods instantiate the filter and link it to all controllers that inherit from this one.

/**
     * Creates a rule to validate user's password freshness.
     * @return array the array of rules to validate against
     */
    public function changePasswordRules()
    {
        return array(
            'days' => 30,
        );
    }

    /**
     * Runs the Password filter
     * @param type $filterChain 
     */
    public function filterChangePassword($filterChain)
    {
        $filter = new ChangePasswordFilter();
        $filter->setRules($this->changePasswordRules());
        $filter->filter($filterChain);
    }

Client Controller Class. This is an example of my SiteController. The changePasswordFilter applies to all actions except for the ones listed after "-".

/**
     * @return array action filters
     */
    public function filters()
    {
        return array(
            'accessControl', // perform access control for CRUD operations
            'changePassword - logout, login, autoLogin, password, securityCode, passwordVerify, verify, autoGeneratePassword',
            'https',
        );
    }

These are two convenience methods I use in my User model. They're used to calculate the days since the user has changed his/her password.

/**
     * Returns the number of days since the user changed their password.
     * @return integer the number of elapsed days
     */
    public function daysSincePasswordChange()
    {
        return $this->calculateInterval($this->password_update_time);
    }

    /**
     * Returns the number of days between two time intervals. If $end is null, 'now' will be used.
     * @param string $start the start time. Should be an English textual description. Ex: 2011-06-28 08:06:53
     * @param string $end the end time. Should be an English textual description. Ex: 2011-06-28 08:06:53
     * @return integer the number of days
     */
    public function calculateInterval($start, $end = null)
    {
        $startTime = strtotime($start);
        $endTime = ($end == null) ? time() : $end;

        $elapsedTime = abs($endTime - $startTime);

        return round($elapsedTime / 86400);
    }

The redirectToPasswordForm method. I include this in WebUser. It is copied from CWebUser->loginRequired() but redirects to the password form, instead of the login form.

public function redirectToPasswordForm()
    {
        if (!Yii::app()->request->getIsAjaxRequest())
            $this->setReturnUrl(Yii::app()->request->getUrl());

        if (($url = $this->changePasswordUrl) !== null)
        {
            if (is_array($url))
            {
                $route = isset($url[0]) ? $url[0] : $app->defaultController;
                $url = Yii::app()->createUrl($route, array_splice($url, 1));
            }
            Yii::app()->request->redirect($url);
        }
        else
            throw new CHttpException(403, Yii::t('yii', 'Change Password Required'));
    }

The rule setup in Controller.php, 'days' => 30,, is obviously very simple. It can be easily extended. For example, users of different roles might have different freshness requirements - Admin users - 10 days, regular users - 30 days, super admins - never etc. You'll just need to modify ChangePasswordFilter->setRules() and ChangePasswordRule->changePasswordRequired() to loop over the list of rules and perform your needed logic.

Hope this helps - feedback welcome.

Matt

14 0
10 followers
Viewed: 29 554 times
Version: Unknown (update)
Category: Tutorials
Written by: waterloomatt
Last updated by: waterloomatt
Created on: Sep 16, 2011
Last updated: 12 years ago
Update Article

Revisions

View all history

Related Articles