CSteipp has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/92037


Change subject: Password Expiration
......................................................................

Password Expiration

Add functionality to expire users' passwords:
 * Adds column to the user table to keep a password expiration
 * Adds a default grace period of 7 days, where if the user's password
   is expired, they can still login, but are encouraged to reset their
   password.
 * Adds hook 'LoginPasswordResetMessage' to update reset message, in
   case an extension wants to vary the message on a particular reset
   event.
 * Adds hook 'ResetPasswordExpiration' to allow extensions to add a
   default expiration date when the user resets their password.

Also doens't do a successful reset if the user is "changing" their
password to their existing password.

Bug: 54997

Change-Id: I92a9fc63b409b182b1d7b48781d73fc7216f8061
---
M docs/hooks.txt
M includes/DefaultSettings.php
M includes/User.php
M includes/specials/SpecialChangePassword.php
M includes/specials/SpecialUserlogin.php
M languages/messages/MessagesEn.php
M languages/messages/MessagesQqq.php
7 files changed, 126 insertions(+), 16 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/core 
refs/changes/37/92037/1

diff --git a/docs/hooks.txt b/docs/hooks.txt
index 2d1001b..3e902a4 100644
--- a/docs/hooks.txt
+++ b/docs/hooks.txt
@@ -1553,6 +1553,12 @@
 $retval: a LoginForm class constant with authenticateUserData() return
   value (SUCCESS, WRONG_PASS, etc.).
 
+'LoginPasswordResetMessage': User is being requested to reset their password 
on login.
+Use this hook to change the Message that will be output on 
Special:ChangePassword.
+&$msg: Message object that will be shown to the user
+$expDate: User's password expiration date, in case you need to vary the 
message on a
+particular expiration event. Will be false if the password isn't expired.
+
 'LogLine': Processes a single log entry on Special:Log.
 $log_type: string for the type of log entry (e.g. 'move'). Corresponds to
   logging.log_type database field.
@@ -1957,6 +1963,10 @@
 &$skin: A variable reference you may set a Skin instance or string key on to
   override the skin that will be used for the context.
 
+'ResetPasswordExpiration': Allow extensions to set a default password 
expiration
+$user: The user having their password expiration reset
+&$newExpire: The new expiration date
+
 'ResetSessionID': Called from wfResetSessionID
 $oldSessionID: old session id
 $newSessionID: new session id
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 98c583b..52f2b6e 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -1359,6 +1359,12 @@
 $wgUserEmailConfirmationTokenExpiry = 7 * 24 * 60 * 60;
 
 /**
+ * If a user's password is expired, the number of seconds when they can still 
login,
+ * and cancel their password change, but are sent to the password change form 
on each login.
+ */
+$wgPasswordExpireGrace = 3600 * 24 * 7; // 7 days
+
+/**
  * SMTP Mode.
  *
  * For using a direct (authenticated) SMTP server connection.
diff --git a/includes/User.php b/includes/User.php
index 12912e1..fa7209d 100644
--- a/includes/User.php
+++ b/includes/User.php
@@ -90,6 +90,7 @@
                'mEmailAuthenticated',
                'mEmailToken',
                'mEmailTokenExpires',
+               'mPasswordExpires',
                'mRegistration',
                'mEditCount',
                // user_groups table
@@ -184,7 +185,7 @@
        var $mId, $mName, $mRealName, $mPassword, $mNewpassword, $mNewpassTime,
                $mEmail, $mTouched, $mToken, $mEmailAuthenticated,
                $mEmailToken, $mEmailTokenExpires, $mRegistration, $mEditCount,
-               $mGroups, $mOptionOverrides;
+               $mGroups, $mOptionOverrides, $mPasswordExpires;
        //@}
 
        /**
@@ -736,6 +737,60 @@
        }
 
        /**
+        * Expire a user's password
+        * @param $ts Mixed: optional timestamp to convert, default 0 for the 
current time
+        */
+       public function expirePassword( $ts = 0 ) {
+               $this->load();
+               $timestamp = wfTimestamp( TS_MW, $ts );
+               $this->mPasswordExpires = $timestamp;
+               $this->saveSettings();
+       }
+
+       /**
+        * Clear the password expiration for a user
+        */
+       public function resetPasswordExpiration() {
+               $this->load();
+               $newExpire = null;
+               // Give extensions a chance to force an expiration
+               wfRunHooks( 'ResetPasswordExpiration', array( $this, 
&$newExpire ) );
+               $this->mPasswordExpires = $newExpire;
+       }
+
+       /**
+        * Check if the user's password is expired.
+        * TODO: Put this and password length into a PasswordPolicy object
+        * @return string|bool The expirateion type, or false if not expired
+        *      hard: A password change is required to login
+        *      soft: Allow login, but encourage password change
+        *      false: Password is not expired
+        */
+       public function getPasswordExpired() {
+               global $wgPasswordExpireGrace;
+               $expired = false;
+
+               $expiration = $this->getPasswordExpireDate();
+               $expUnix = wfTimestamp( TS_UNIX, $expiration );
+               $graceExpired = ( wfTimestamp() > $expUnix + 
$wgPasswordExpireGrace );
+               if ( $expiration !== null && $expiration < wfTimestamp( TS_MW ) 
) {
+                       $expired = $graceExpired ? 'hard' : 'soft';
+               }
+               return $expired;
+       }
+
+       /**
+        * Get this user's password expiration date. Since this may be using
+        * the cached User object, we assume that whatever mechanism is setting
+        * the expiration date is also expiring the User cache.
+        * @return string|false the datestamp of the expiration, or null if not 
set
+        */
+       public function getPasswordExpireDate() {
+               $this->load();
+               return $this->mPasswordExpires;
+       }
+
+       /**
         * Does a string look like an e-mail address?
         *
         * This validates an email address using an HTML5 specification found 
at:
@@ -890,6 +945,7 @@
                $this->mEmailAuthenticated = null;
                $this->mEmailToken = '';
                $this->mEmailTokenExpires = null;
+               $this->mPasswordExpires = null;
                $this->mRegistration = wfTimestamp( TS_MW );
                $this->mGroups = array();
 
@@ -1097,6 +1153,7 @@
                        $this->mEmailAuthenticated = wfTimestampOrNull( TS_MW, 
$row->user_email_authenticated );
                        $this->mEmailToken = $row->user_email_token;
                        $this->mEmailTokenExpires = wfTimestampOrNull( TS_MW, 
$row->user_email_token_expires );
+                       $this->mPasswordExpires = wfTimestampOrNull( TS_MW, 
$row->user_password_expires );
                        $this->mRegistration = wfTimestampOrNull( TS_MW, 
$row->user_registration );
                } else {
                        $all = false;
@@ -3295,6 +3352,7 @@
                                'user_token' => strval( $this->mToken ),
                                'user_email_token' => $this->mEmailToken,
                                'user_email_token_expires' => 
$dbw->timestampOrNull( $this->mEmailTokenExpires ),
+                               'user_password_expires' => 
$dbw->timestampOrNull( $this->mPasswordExpires ),
                        ), array( /* WHERE */
                                'user_id' => $this->mId
                        ), __METHOD__
@@ -4769,6 +4827,7 @@
                        'user_email_authenticated',
                        'user_email_token',
                        'user_email_token_expires',
+                       'user_password_expires',
                        'user_registration',
                        'user_editcount',
                );
diff --git a/includes/specials/SpecialChangePassword.php 
b/includes/specials/SpecialChangePassword.php
index 129e919..0a96c60 100644
--- a/includes/specials/SpecialChangePassword.php
+++ b/includes/specials/SpecialChangePassword.php
@@ -28,7 +28,7 @@
  */
 class SpecialChangePassword extends UnlistedSpecialPage {
 
-       protected $mUserName, $mOldpass, $mNewpass, $mRetype, $mDomain;
+       protected $mUserName, $mOldpass, $mNewpass, $mRetype, $mDomain, 
$mMessage;
 
        public function __construct() {
                parent::__construct( 'ChangePassword', 'editmyprivateinfo' );
@@ -119,6 +119,10 @@
                $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' 
=> 'error' ), $msg ) );
        }
 
+       public function setChangeMessage( $msg ) {
+               $this->mMessage = $msg;
+       }
+
        function showForm() {
                global $wgCookieExpiration;
 
@@ -126,6 +130,11 @@
                if ( !$this->mUserName ) {
                        $this->mUserName = $user->getName();
                }
+
+               if ( $this->mMessage && $this->mMessage instanceof Message ) {
+                       $this->getOutput()->addHTML( $this->mMessage->parse() );
+               }
+
                $rememberMe = '';
                if ( !$user->isLoggedIn() ) {
                        $rememberMe = '<tr>' .
@@ -257,15 +266,22 @@
                        );
                }
 
+               if ( !$user->checkTemporaryPassword( $this->mOldpass ) && 
!$user->checkPassword( $this->mOldpass ) ) {
+                       wfRunHooks( 'PrefsPasswordAudit', array( $user, 
$newpass, 'wrongpassword' ) );
+                       throw new PasswordError( $this->msg( 
'resetpass-wrong-oldpass' )->text() );
+               }
+
+               // User is resetting their password to their old password
+               if ( $this->mOldpass === $newpass ) {
+                       throw new PasswordError( $this->msg( 
'resetpass-recycled' )->text() );
+               }
+
+               // Do AbortChangePassword after checking mOldpass, so we don't 
leak information
+               // by possibly aborting a new password before verifying the old 
password.
                $abortMsg = 'resetpass-abort-generic';
                if ( !wfRunHooks( 'AbortChangePassword', array( $user, 
$this->mOldpass, $newpass, &$abortMsg ) ) ) {
                        wfRunHooks( 'PrefsPasswordAudit', array( $user, 
$newpass, 'abortreset' ) );
                        throw new PasswordError( $this->msg( $abortMsg 
)->text() );
-               }
-
-               if ( !$user->checkTemporaryPassword( $this->mOldpass ) && 
!$user->checkPassword( $this->mOldpass ) ) {
-                       wfRunHooks( 'PrefsPasswordAudit', array( $user, 
$newpass, 'wrongpassword' ) );
-                       throw new PasswordError( $this->msg( 
'resetpass-wrong-oldpass' )->text() );
                }
 
                // Please reset throttle for successful logins, thanks!
@@ -287,7 +303,7 @@
                        // changing the password also modifies the user's token.
                        $user->setCookies();
                }
-
+               $user->resetPasswordExpiration();
                $user->saveSettings();
        }
 
diff --git a/includes/specials/SpecialUserlogin.php 
b/includes/specials/SpecialUserlogin.php
index 2b60ca2..b450841 100644
--- a/includes/specials/SpecialUserlogin.php
+++ b/includes/specials/SpecialUserlogin.php
@@ -519,7 +519,7 @@
         * @return int
         */
        public function authenticateUserData() {
-               global $wgUser, $wgAuth;
+               global $wgUser, $wgAuth, $wgPasswordExpireGrace;
 
                $this->load();
 
@@ -615,6 +615,7 @@
                                // At this point we just return an appropriate 
code/ indicating
                                // that the UI should show a password reset 
form; bot inter-
                                // faces etc will probably just fail cleanly 
here.
+                               $this->mAbortLoginErrorMsg = 
'resetpass-temp-emailed';
                                $retval = self::RESET_PASS;
                        } else {
                                $retval = ( $this->mPassword == '' ) ? 
self::EMPTY_PASS : self::WRONG_PASS;
@@ -622,6 +623,10 @@
                } elseif ( $wgBlockDisablesLogin && $u->isBlocked() ) {
                        // If we've enabled it, make it so that a blocked user 
cannot login
                        $retval = self::USER_BLOCKED;
+               } elseif ( $u->getPasswordExpired() == 'hard' ) {
+                       // Force reset now, without logging in
+                       $retval = self::RESET_PASS;
+                       $this->mAbortLoginErrorMsg = 'resetpass-expired';
                } else {
                        $wgAuth->updateUser( $u );
                        $wgUser = $u;
@@ -775,7 +780,11 @@
                                        $this->getContext()->setLanguage( 
$userLang );
                                        // Reset SessionID on Successful login 
(bug 40995)
                                        $this->renewSessionId();
-                                       $this->successfulLogin();
+                                       if ( 
$this->getUser()->getPasswordExpired() == 'soft' ) {
+                                               $this->resetLoginForm( 
$this->msg( 'resetpass-expired-soft' ) );
+                                       } else {
+                                               $this->successfulLogin();
+                                       }
                                } else {
                                        $this->cookieRedirectCheck( 'login' );
                                }
@@ -819,7 +828,7 @@
                                break;
                        case self::RESET_PASS:
                                $error = $this->mAbortLoginErrorMsg ?: 
'resetpass_announce';
-                               $this->resetLoginForm( $this->msg( $error 
)->text() );
+                               $this->resetLoginForm( $this->msg( $error ) );
                                break;
                        case self::CREATE_BLOCKED:
                                $this->userBlockedMessage( 
$this->getUser()->isBlockedFromCreateAccount() );
@@ -845,11 +854,13 @@
        }
 
        /**
-        * @param $error string
+        * @param $msg Message
         */
-       function resetLoginForm( $error ) {
-               $this->getOutput()->addHTML( Xml::element( 'p', array( 'class' 
=> 'error' ), $error ) );
+       function resetLoginForm( $msg ) {
+               $expDate = $this->getUser()->getPasswordExpireDate();
+               wfRunHooks( 'LoginPasswordResetMessage', array( &$msg, $expDate 
) );
                $reset = new SpecialChangePassword();
+               $reset->setChangeMessage( $msg );
                $reset->setContext( $this->getContext() );
                $reset->execute( null );
        }
diff --git a/languages/messages/MessagesEn.php 
b/languages/messages/MessagesEn.php
index faaccd2..0230676 100644
--- a/languages/messages/MessagesEn.php
+++ b/languages/messages/MessagesEn.php
@@ -1266,8 +1266,7 @@
 
 # Change password dialog
 'resetpass'                 => 'Change password',
-'resetpass_announce'        => 'You logged in with a temporary emailed code.
-To finish logging in, you must set a new password here:',
+'resetpass_announce'        => 'To finish logging in, you must set a new 
password here:',
 'resetpass_text'            => '<!-- Add text here -->', # only translate this 
message to other languages if you have to change it
 'resetpass_header'          => 'Change account password',
 'oldpassword'               => 'Old password:',
@@ -1281,8 +1280,13 @@
 'resetpass-submit-cancel'   => 'Cancel',
 'resetpass-wrong-oldpass'   => 'Invalid temporary or current password.
 You may have already successfully changed your password or requested a new 
temporary password.',
+'resetpass-recycled'        => 'Please reset your password to something other 
than your current password.',
+'resetpass-temp-emailed'    => 'You logged in with a temporary emailed code.
+To finish logging in, you must set a new password here:',
 'resetpass-temp-password'   => 'Temporary password:',
 'resetpass-abort-generic'   => 'Password change has been aborted by an 
extension.',
+'resetpass-expired' => '<span class="error">Your password has expired. Please 
set a new password to login.</span>',
+'resetpass-expired-soft' => '<span class="warning">Your password has expired, 
and needs to be reset. Please choose a new password now, or click cancel to 
reset it later. </span>',
 
 # Special:PasswordReset
 'passwordreset'                    => 'Reset password',
diff --git a/languages/messages/MessagesQqq.php 
b/languages/messages/MessagesQqq.php
index a38f17c..8042fee 100644
--- a/languages/messages/MessagesQqq.php
+++ b/languages/messages/MessagesQqq.php
@@ -1604,8 +1604,12 @@
 'resetpass-submit-cancel' => 'Used on [[Special:ResetPass]].
 {{Identical|Cancel}}',
 'resetpass-wrong-oldpass' => 'Error message shown on 
[[Special:ChangePassword]] when the old password is not valid.',
+'resetpass-recycled' => 'Error message shown on [[Special:ChangePassword]] 
when a user attempts to reset their password to their existing password.',
+'resetpass-temp-emailed' => 'Message shown on [[Special:ChangePassword]] when 
a user logs in with a temporary password, and must set a new password.',
 'resetpass-temp-password' => 'The label of the input box for the temporary 
password (received by email) on the form displayed after logging in with a 
temporary password.',
 'resetpass-abort-generic' => 'Generic error message shown on 
[[Special:ChangePassword]] when an extension aborts a password change from a 
hook.',
+'resetpass-expired' => 'Generic error message shown on 
[[Special:ChangePassword]] when a user\'s password is expired',
+'resetpass-expired-soft' => 'Generic marning message shown on 
[[Special:ChangePassword]] when a user needs to reset their password, but they 
are not prevented from logging in at this time',
 
 # Special:PasswordReset
 'passwordreset' => 'Title of [[Special:PasswordReset]].

-- 
To view, visit https://gerrit.wikimedia.org/r/92037
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I92a9fc63b409b182b1d7b48781d73fc7216f8061
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/core
Gerrit-Branch: master
Gerrit-Owner: CSteipp <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to