Reset all tokens on login
[lhc/web/wiklou.git] / includes / user / User.php
index 71023c0..ff3171e 100644 (file)
@@ -674,12 +674,26 @@ class User implements IDBAccessObject {
         * This can optionally create the user if it doesn't exist, and "steal" the
         * account if it does exist.
         *
+        * "Stealing" an existing user is intended to make it impossible for normal
+        * authentication processes to use the account, effectively disabling the
+        * account for normal use:
+        * - Email is invalidated, to prevent account recovery by emailing a
+        *   temporary password and to disassociate the account from the existing
+        *   human.
+        * - The token is set to a magic invalid value, to kill existing sessions
+        *   and to prevent $this->setToken() calls from resetting the token to a
+        *   valid value.
+        * - SessionManager is instructed to prevent new sessions for the user, to
+        *   do things like deauthorizing OAuth consumers.
+        * - AuthManager is instructed to revoke access, to invalidate or remove
+        *   passwords and other credentials.
+        *
         * @param string $name Username
         * @param array $options Options are:
         *  - validate: As for User::getCanonicalName(), default 'valid'
         *  - create: Whether to create the user if it doesn't already exist, default true
-        *  - steal: Whether to reset the account's password and email if it
-        *    already exists, default false
+        *  - steal: Whether to "disable" the account for normal use if it already
+        *    exists, default false
         * @return User|null
         * @since 1.27
         */
@@ -715,9 +729,8 @@ class User implements IDBAccessObject {
                }
                $user = self::newFromRow( $row );
 
-               // A user is considered to exist as a non-system user if it has a
-               // password set, or a temporary password set, or an email set, or a
-               // non-invalid token.
+               // A user is considered to exist as a non-system user if it can
+               // authenticate, or has an email set, or has a non-invalid token.
                if ( !$user->mEmail && $user->mToken === self::INVALID_TOKEN ) {
                        if ( $wgDisableAuthManager ) {
                                $passwordFactory = new PasswordFactory();
@@ -1467,6 +1480,24 @@ class User implements IDBAccessObject {
                return $toPromote;
        }
 
+       /**
+        * Builds update conditions. Additional conditions may be added to $conditions to
+        * protected against race conditions using a compare-and-set (CAS) mechanism
+        * based on comparing $this->mTouched with the user_touched field.
+        *
+        * @param DatabaseBase $db
+        * @param array $conditions WHERE conditions for use with DatabaseBase::update
+        * @return array WHERE conditions for use with DatabaseBase::update
+        */
+       protected function makeUpdateConditions( DatabaseBase $db, array $conditions ) {
+               if ( $this->mTouched ) {
+                       // CAS check: only update if the row wasn't changed sicne it was loaded.
+                       $conditions['user_touched'] = $db->timestamp( $this->mTouched );
+               }
+
+               return $conditions;
+       }
+
        /**
         * Bump user_touched if it didn't change since this object was loaded
         *
@@ -1484,16 +1515,14 @@ class User implements IDBAccessObject {
                }
 
                // Get a new user_touched that is higher than the old one
-               $oldTouched = $this->mTouched;
                $newTouched = $this->newTouchedTimestamp();
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->update( 'user',
                        [ 'user_touched' => $dbw->timestamp( $newTouched ) ],
-                       [
+                       $this->makeUpdateConditions( $dbw, [
                                'user_id' => $this->mId,
-                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
-                       ],
+                       ] ),
                        __METHOD__
                );
                $success = ( $dbw->affectedRows() > 0 );
@@ -3859,6 +3888,7 @@ class User implements IDBAccessObject {
                if ( !$session->canSetUser() ) {
                        \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
                                ->warning( __METHOD__ . ": Cannot log out of an immutable session" );
+                       $error = 'immutable';
                } elseif ( !$session->getUser()->equals( $this ) ) {
                        \MediaWiki\Logger\LoggerFactory::getInstance( 'session' )
                                ->warning( __METHOD__ .
@@ -3866,6 +3896,7 @@ class User implements IDBAccessObject {
                                );
                        // But we still may as well make this user object anon
                        $this->clearInstanceCache( 'defaults' );
+                       $error = 'wronguser';
                } else {
                        $this->clearInstanceCache( 'defaults' );
                        $delay = $session->delaySave();
@@ -3873,8 +3904,15 @@ class User implements IDBAccessObject {
                        $session->setLoggedOutTimestamp( time() );
                        $session->setUser( new User );
                        $session->set( 'wsUserID', 0 ); // Other code expects this
+                       $session->resetAllTokens();
                        ScopedCallback::consume( $delay );
+                       $error = false;
                }
+               \MediaWiki\Logger\LoggerFactory::getInstance( 'authmanager' )->info( 'Logout', [
+                       'event' => 'logout',
+                       'successful' => $error === false,
+                       'status' => $error ?: 'success',
+               ] );
        }
 
        /**
@@ -3900,7 +3938,6 @@ class User implements IDBAccessObject {
                // Get a new user_touched that is higher than the old one.
                // This will be used for a CAS check as a last-resort safety
                // check against race conditions and slave lag.
-               $oldTouched = $this->mTouched;
                $newTouched = $this->newTouchedTimestamp();
 
                $dbw = wfGetDB( DB_MASTER );
@@ -3914,10 +3951,9 @@ class User implements IDBAccessObject {
                                'user_token' => strval( $this->mToken ),
                                'user_email_token' => $this->mEmailToken,
                                'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ),
-                       ], [ /* WHERE */
+                       ], $this->makeUpdateConditions( $dbw, [ /* WHERE */
                                'user_id' => $this->mId,
-                               'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check
-                       ], __METHOD__
+                       ] ), __METHOD__
                );
 
                if ( !$dbw->affectedRows() ) {