Reset all tokens on login
[lhc/web/wiklou.git] / includes / user / User.php
index 7c32c3b..ff3171e 100644 (file)
@@ -23,6 +23,9 @@
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Session\SessionManager;
 use MediaWiki\Session\Token;
+use MediaWiki\Auth\AuthManager;
+use MediaWiki\Auth\AuthenticationResponse;
+use MediaWiki\Auth\AuthenticationRequest;
 
 /**
  * String Some punctuation to prevent editing from broken text-mangling proxies.
@@ -127,6 +130,7 @@ class User implements IDBAccessObject {
                'createpage',
                'createtalk',
                'delete',
+               'deletechangetags',
                'deletedhistory',
                'deletedtext',
                'deletelogentry',
@@ -475,7 +479,7 @@ class User implements IDBAccessObject {
         */
        protected static function getInProcessCache() {
                if ( !self::$inProcessCache ) {
-                       self::$inProcessCache = new HashBagOStuff( ['maxKeys' => 10] );
+                       self::$inProcessCache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
                }
                return self::$inProcessCache;
        }
@@ -498,7 +502,10 @@ class User implements IDBAccessObject {
                $data = $processCache->get( $key );
                if ( !is_array( $data ) ) {
                        $data = $cache->get( $key );
-                       if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) {
+                       if ( !is_array( $data )
+                               || !isset( $data['mVersion'] )
+                               || $data['mVersion'] < self::VERSION
+                       ) {
                                // Object is expired
                                return false;
                        }
@@ -667,15 +674,32 @@ 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
         */
        public static function newSystemUser( $name, $options = [] ) {
+               global $wgDisableAuthManager;
+
                $options += [
                        'validate' => 'valid',
                        'create' => true,
@@ -687,13 +711,15 @@ class User implements IDBAccessObject {
                        return null;
                }
 
+               $fields = self::selectFields();
+               if ( $wgDisableAuthManager ) {
+                       $fields = array_merge( $fields, [ 'user_password', 'user_newpassword' ] );
+               }
+
                $dbw = wfGetDB( DB_MASTER );
                $row = $dbw->selectRow(
                        'user',
-                       array_merge(
-                               self::selectFields(),
-                               [ 'user_password', 'user_newpassword' ]
-                       ),
+                       $fields,
                        [ 'user_name' => $name ],
                        __METHOD__
                );
@@ -703,43 +729,52 @@ 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.
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               try {
-                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $password = PasswordFactory::newInvalidPassword();
-               }
-               try {
-                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newpassword = PasswordFactory::newInvalidPassword();
-               }
-               if ( !$password instanceof InvalidPassword || !$newpassword instanceof InvalidPassword
-                       || $user->mEmail || $user->mToken !== self::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();
+                               $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                               try {
+                                       $password = $passwordFactory->newFromCiphertext( $row->user_password );
+                               } catch ( PasswordError $e ) {
+                                       wfDebug( 'Invalid password hash found in database.' );
+                                       $password = PasswordFactory::newInvalidPassword();
+                               }
+                               try {
+                                       $newpassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+                               } catch ( PasswordError $e ) {
+                                       wfDebug( 'Invalid password hash found in database.' );
+                                       $newpassword = PasswordFactory::newInvalidPassword();
+                               }
+                               $canAuthenticate = !$password instanceof InvalidPassword ||
+                                       !$newpassword instanceof InvalidPassword;
+                       } else {
+                               $canAuthenticate = AuthManager::singleton()->userCanAuthenticate( $name );
+                       }
+               }
+               if ( $user->mEmail || $user->mToken !== self::INVALID_TOKEN || $canAuthenticate ) {
                        // User exists. Steal it?
                        if ( !$options['steal'] ) {
                                return null;
                        }
 
-                       $nopass = PasswordFactory::newInvalidPassword()->toString();
+                       if ( $wgDisableAuthManager ) {
+                               $nopass = PasswordFactory::newInvalidPassword()->toString();
+                               $dbw->update(
+                                       'user',
+                                       [
+                                               'user_password' => $nopass,
+                                               'user_newpassword' => $nopass,
+                                               'user_newpass_time' => null,
+                                       ],
+                                       [ 'user_id' => $user->getId() ],
+                                       __METHOD__
+                               );
+                       } else {
+                               AuthManager::singleton()->revokeAccessForUser( $name );
+                       }
 
-                       $dbw->update(
-                               'user',
-                               [
-                                       'user_password' => $nopass,
-                                       'user_newpassword' => $nopass,
-                                       'user_newpass_time' => null,
-                               ],
-                               [ 'user_id' => $user->getId() ],
-                               __METHOD__
-                       );
                        $user->invalidateEmail();
                        $user->mToken = self::INVALID_TOKEN;
                        $user->saveSettings();
@@ -1078,8 +1113,9 @@ class User implements IDBAccessObject {
                }
 
                // Reject various classes of invalid names
-               global $wgAuth;
-               $name = $wgAuth->getCanonicalName( $t->getText() );
+               $name = AuthManager::callLegacyAuthPlugin(
+                       'getCanonicalName', [ $t->getText() ], $t->getText()
+               );
 
                switch ( $validate ) {
                        case false:
@@ -1404,7 +1440,7 @@ class User implements IDBAccessObject {
         * @see $wgAutopromoteOnce
         */
        public function addAutopromoteOnceGroups( $event ) {
-               global $wgAutopromoteOnceLogInRC, $wgAuth;
+               global $wgAutopromoteOnceLogInRC;
 
                if ( wfReadOnly() || !$this->getId() ) {
                        return [];
@@ -1425,7 +1461,7 @@ class User implements IDBAccessObject {
                }
                // update groups in external authentication database
                Hooks::run( 'UserGroupsChanged', [ $this, $toPromote, [], false, false ] );
-               $wgAuth->updateExternalDBGroups( $this, $toPromote );
+               AuthManager::callLegacyAuthPlugin( 'updateExternalDBGroups', [ $this, $toPromote ] );
 
                $newGroups = array_merge( $oldGroups, $toPromote ); // all groups
 
@@ -1444,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
         *
@@ -1461,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 );
@@ -2040,9 +2092,8 @@ class User implements IDBAccessObject {
                if ( $this->mLocked !== null ) {
                        return $this->mLocked;
                }
-               global $wgAuth;
-               $authUser = $wgAuth->getUserInstance( $this );
-               $this->mLocked = (bool)$authUser->isLocked();
+               $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+               $this->mLocked = $authUser && $authUser->isLocked();
                Hooks::run( 'UserIsLocked', [ $this, &$this->mLocked ] );
                return $this->mLocked;
        }
@@ -2058,9 +2109,8 @@ class User implements IDBAccessObject {
                }
                $this->getBlockedStatus();
                if ( !$this->mHideName ) {
-                       global $wgAuth;
-                       $authUser = $wgAuth->getUserInstance( $this );
-                       $this->mHideName = (bool)$authUser->isHidden();
+                       $authUser = AuthManager::callLegacyAuthPlugin( 'getUserInstance', [ &$this ], null );
+                       $this->mHideName = $authUser && $authUser->isHidden();
                        Hooks::run( 'UserIsHidden', [ $this, &$this->mHideName ] );
                }
                return $this->mHideName;
@@ -2466,13 +2516,17 @@ class User implements IDBAccessObject {
         * wipes it, so the account cannot be logged in until
         * a new password is set, for instance via e-mail.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $str New password to set
         * @throws PasswordError On failure
         * @return bool
         */
        public function setPassword( $str ) {
-               global $wgAuth;
+               global $wgAuth, $wgDisableAuthManager;
+
+               if ( !$wgDisableAuthManager ) {
+                       return $this->setPasswordInternal( $str );
+               }
 
                if ( $str !== null ) {
                        if ( !$wgAuth->allowPasswordChange() ) {
@@ -2489,7 +2543,6 @@ class User implements IDBAccessObject {
                        throw new PasswordError( wfMessage( 'externaldberror' )->text() );
                }
 
-               $this->setToken();
                $this->setOption( 'watchlisttoken', false );
                $this->setPasswordInternal( $str );
 
@@ -2499,16 +2552,19 @@ class User implements IDBAccessObject {
        /**
         * Set the password and reset the random token unconditionally.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string|null $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to log in
         *  through the web interface.
         */
        public function setInternalPassword( $str ) {
-               global $wgAuth;
+               global $wgAuth, $wgDisableAuthManager;
+
+               if ( !$wgDisableAuthManager ) {
+                       $this->setPasswordInternal( $str );
+               }
 
                if ( $wgAuth->allowSetLocalPassword() ) {
-                       $this->setToken();
                        $this->setOption( 'watchlisttoken', false );
                        $this->setPasswordInternal( $str );
                }
@@ -2520,31 +2576,68 @@ class User implements IDBAccessObject {
         * @param string|null $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to log in
         *  through the web interface.
+        * @return bool Success
         */
        private function setPasswordInternal( $str ) {
-               $id = self::idFromName( $this->getName(), self::READ_LATEST );
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+               global $wgDisableAuthManager;
+
+               if ( $wgDisableAuthManager ) {
+                       $id = self::idFromName( $this->getName(), self::READ_LATEST );
+                       if ( $id == 0 ) {
+                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+                       }
+
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $dbw = wfGetDB( DB_MASTER );
+                       $dbw->update(
+                               'user',
+                               [
+                                       'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
+                                       'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
+                                       'user_newpass_time' => $dbw->timestampOrNull( null ),
+                               ],
+                               [
+                                       'user_id' => $id,
+                               ],
+                               __METHOD__
+                       );
+
+                       // When the main password is changed, invalidate all bot passwords too
+                       BotPassword::invalidateAllPasswordsForUser( $this->getName() );
+               } else {
+                       $manager = AuthManager::singleton();
+
+                       // If the user doesn't exist yet, fail
+                       if ( !$manager->userExists( $this->getName() ) ) {
+                               throw new LogicException( 'Cannot set a password for a user that is not in the database.' );
+                       }
+
+                       $data = [
+                               'username' => $this->getName(),
+                               'password' => $str,
+                               'retype' => $str,
+                       ];
+                       $reqs = $manager->getAuthenticationRequests( AuthManager::ACTION_CHANGE, $this );
+                       $reqs = AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
+                       foreach ( $reqs as $req ) {
+                               $status = $manager->allowsAuthenticationDataChange( $req );
+                               if ( !$status->isOk() ) {
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Password change rejected: ' . $status->getWikiText() );
+                                       return false;
+                               }
+                       }
+                       foreach ( $reqs as $req ) {
+                               $manager->changeAuthenticationData( $req );
+                       }
+
+                       $this->setOption( 'watchlisttoken', false );
                }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->update(
-                       'user',
-                       [
-                               'user_password' => $passwordFactory->newFromPlaintext( $str )->toString(),
-                               'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(),
-                               'user_newpass_time' => $dbw->timestampOrNull( null ),
-                       ],
-                       [
-                               'user_id' => $id,
-                       ],
-                       __METHOD__
-               );
+               SessionManager::singleton()->invalidateSessionsForUser( $this );
 
-               // When the main password is changed, invalidate all bot passwords too
-               BotPassword::invalidateAllPasswordsForUser( $this->getName() );
+               return true;
        }
 
        /**
@@ -2606,63 +2699,74 @@ class User implements IDBAccessObject {
        /**
         * Set the password for a password reminder or new account email
         *
-        * @deprecated since 1.27, AuthManager is coming
+        * @deprecated Removed in 1.27. Use PasswordReset instead.
         * @param string $str New password to set or null to set an invalid
         *  password hash meaning that the user will not be able to use it
         * @param bool $throttle If true, reset the throttle timestamp to the present
         */
        public function setNewpassword( $str, $throttle = true ) {
-               $id = $this->getId();
-               if ( $id == 0 ) {
-                       throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
-               }
+               global $wgDisableAuthManager;
 
-               $dbw = wfGetDB( DB_MASTER );
+               if ( $wgDisableAuthManager ) {
+                       $id = $this->getId();
+                       if ( $id == 0 ) {
+                               throw new LogicException( 'Cannot set new password for a user that is not in the database.' );
+                       }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $update = [
-                       'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
-               ];
+                       $dbw = wfGetDB( DB_MASTER );
 
-               if ( $str === null ) {
-                       $update['user_newpass_time'] = null;
-               } elseif ( $throttle ) {
-                       $update['user_newpass_time'] = $dbw->timestamp();
-               }
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $update = [
+                               'user_newpassword' => $passwordFactory->newFromPlaintext( $str )->toString(),
+                       ];
+
+                       if ( $str === null ) {
+                               $update['user_newpass_time'] = null;
+                       } elseif ( $throttle ) {
+                               $update['user_newpass_time'] = $dbw->timestamp();
+                       }
 
-               $dbw->update( 'user', $update, [ 'user_id' => $id ], __METHOD__ );
+                       $dbw->update( 'user', $update, [ 'user_id' => $id ], __METHOD__ );
+               } else {
+                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
+               }
        }
 
        /**
         * Has password reminder email been sent within the last
         * $wgPasswordReminderResendTime hours?
+        * @deprecated Removed in 1.27. See above.
         * @return bool
         */
        public function isPasswordReminderThrottled() {
-               global $wgPasswordReminderResendTime;
+               global $wgPasswordReminderResendTime, $wgDisableAuthManager;
 
-               if ( !$wgPasswordReminderResendTime ) {
-                       return false;
-               }
+               if ( $wgDisableAuthManager ) {
+                       if ( !$wgPasswordReminderResendTime ) {
+                               return false;
+                       }
 
-               $this->load();
+                       $this->load();
 
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
-               $newpassTime = $db->selectField(
-                       'user',
-                       'user_newpass_time',
-                       [ 'user_id' => $this->getId() ],
-                       __METHOD__
-               );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
+                       $newpassTime = $db->selectField(
+                               'user',
+                               'user_newpass_time',
+                               [ 'user_id' => $this->getId() ],
+                               __METHOD__
+                       );
 
-               if ( $newpassTime === null ) {
-                       return false;
+                       if ( $newpassTime === null ) {
+                               return false;
+                       }
+                       $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
+                       return time() < $expiry;
+               } else {
+                       throw new BadMethodCallException( __METHOD__ . ' has been removed in 1.27' );
                }
-               $expiry = wfTimestamp( TS_UNIX, $newpassTime ) + $wgPasswordReminderResendTime * 3600;
-               return time() < $expiry;
        }
 
        /**
@@ -3398,6 +3502,21 @@ class User implements IDBAccessObject {
                return !$this->isLoggedIn();
        }
 
+       /**
+        * @return bool Whether this user is flagged as being a bot role account
+        * @since 1.28
+        */
+       public function isBot() {
+               if ( in_array( 'bot', $this->getGroups() ) && $this->isAllowed( 'bot' ) ) {
+                       return true;
+               }
+
+               $isBot = false;
+               Hooks::run( "UserIsBot", [ $this, &$isBot ] );
+
+               return $isBot;
+       }
+
        /**
         * Check if user is allowed to access a feature / make an action
         *
@@ -3500,7 +3619,7 @@ class User implements IDBAccessObject {
         */
        public function isWatched( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
                if ( $title->isWatchable() && ( !$checkRights || $this->isAllowed( 'viewmywatchlist' ) ) ) {
-                       return WatchedItemStore::getDefaultInstance()->isWatched( $this, $title );
+                       return MediaWikiServices::getInstance()->getWatchedItemStore()->isWatched( $this, $title );
                }
                return false;
        }
@@ -3514,7 +3633,7 @@ class User implements IDBAccessObject {
         */
        public function addWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
                if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
-                       WatchedItemStore::getDefaultInstance()->addWatchBatchForUser(
+                       MediaWikiServices::getInstance()->getWatchedItemStore()->addWatchBatchForUser(
                                $this,
                                [ $title->getSubjectPage(), $title->getTalkPage() ]
                        );
@@ -3531,8 +3650,9 @@ class User implements IDBAccessObject {
         */
        public function removeWatch( $title, $checkRights = self::CHECK_USER_RIGHTS ) {
                if ( !$checkRights || $this->isAllowed( 'editmywatchlist' ) ) {
-                       WatchedItemStore::getDefaultInstance()->removeWatch( $this, $title->getSubjectPage() );
-                       WatchedItemStore::getDefaultInstance()->removeWatch( $this, $title->getTalkPage() );
+                       $store = MediaWikiServices::getInstance()->getWatchedItemStore();
+                       $store->removeWatch( $this, $title->getSubjectPage() );
+                       $store->removeWatch( $this, $title->getTalkPage() );
                }
                $this->invalidateCache();
        }
@@ -3601,7 +3721,7 @@ class User implements IDBAccessObject {
                        $force = 'force';
                }
 
-               WatchedItemStore::getDefaultInstance()
+               MediaWikiServices::getInstance()->getWatchedItemStore()
                        ->resetNotificationTimestamp( $this, $title, $force, $oldid );
        }
 
@@ -3768,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__ .
@@ -3775,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();
@@ -3782,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',
+               ] );
        }
 
        /**
@@ -3809,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 );
@@ -3823,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() ) {
@@ -4130,110 +4257,140 @@ class User implements IDBAccessObject {
 
        /**
         * Check to see if the given clear-text password is one of the accepted passwords
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $password User password
         * @return bool True if the given password is correct, otherwise False
         */
        public function checkPassword( $password ) {
-               global $wgAuth, $wgLegacyEncoding;
+               global $wgAuth, $wgLegacyEncoding, $wgDisableAuthManager;
 
-               $this->load();
+               if ( $wgDisableAuthManager ) {
+                       $this->load();
 
-               // Some passwords will give a fatal Status, which means there is
-               // some sort of technical or security reason for this password to
-               // be completely invalid and should never be checked (e.g., T64685)
-               if ( !$this->checkPasswordValidity( $password )->isOK() ) {
-                       return false;
-               }
+                       // Some passwords will give a fatal Status, which means there is
+                       // some sort of technical or security reason for this password to
+                       // be completely invalid and should never be checked (e.g., T64685)
+                       if ( !$this->checkPasswordValidity( $password )->isOK() ) {
+                               return false;
+                       }
 
-               // Certain authentication plugins do NOT want to save
-               // domain passwords in a mysql database, so we should
-               // check this (in case $wgAuth->strict() is false).
-               if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
-                       return true;
-               } elseif ( $wgAuth->strict() ) {
-                       // Auth plugin doesn't allow local authentication
-                       return false;
-               } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
-                       // Auth plugin doesn't allow local authentication for this user name
-                       return false;
-               }
+                       // Certain authentication plugins do NOT want to save
+                       // domain passwords in a mysql database, so we should
+                       // check this (in case $wgAuth->strict() is false).
+                       if ( $wgAuth->authenticate( $this->getName(), $password ) ) {
+                               return true;
+                       } elseif ( $wgAuth->strict() ) {
+                               // Auth plugin doesn't allow local authentication
+                               return false;
+                       } elseif ( $wgAuth->strictUserAuth( $this->getName() ) ) {
+                               // Auth plugin doesn't allow local authentication for this user name
+                               return false;
+                       }
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
 
-               try {
-                       $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
-                               'user', 'user_password', [ 'user_id' => $this->getId() ], __METHOD__
-                       ) );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $mPassword = PasswordFactory::newInvalidPassword();
-               }
+                       try {
+                               $mPassword = $passwordFactory->newFromCiphertext( $db->selectField(
+                                       'user', 'user_password', [ 'user_id' => $this->getId() ], __METHOD__
+                               ) );
+                       } catch ( PasswordError $e ) {
+                               wfDebug( 'Invalid password hash found in database.' );
+                               $mPassword = PasswordFactory::newInvalidPassword();
+                       }
 
-               if ( !$mPassword->equals( $password ) ) {
-                       if ( $wgLegacyEncoding ) {
-                               // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
-                               // Check for this with iconv
-                               $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
-                               if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
+                       if ( !$mPassword->equals( $password ) ) {
+                               if ( $wgLegacyEncoding ) {
+                                       // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
+                                       // Check for this with iconv
+                                       $cp1252Password = iconv( 'UTF-8', 'WINDOWS-1252//TRANSLIT', $password );
+                                       if ( $cp1252Password === $password || !$mPassword->equals( $cp1252Password ) ) {
+                                               return false;
+                                       }
+                               } else {
                                        return false;
                                }
-                       } else {
-                               return false;
                        }
-               }
 
-               if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
-                       $this->setPasswordInternal( $password );
-               }
+                       if ( $passwordFactory->needsUpdate( $mPassword ) && !wfReadOnly() ) {
+                               $this->setPasswordInternal( $password );
+                       }
 
-               return true;
+                       return true;
+               } else {
+                       $manager = AuthManager::singleton();
+                       $reqs = AuthenticationRequest::loadRequestsFromSubmission(
+                               $manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN ),
+                               [
+                                       'username' => $this->getName(),
+                                       'password' => $password,
+                               ]
+                       );
+                       $res = AuthManager::singleton()->beginAuthentication( $reqs, 'null:' );
+                       switch ( $res->status ) {
+                               case AuthenticationResponse::PASS:
+                                       return true;
+                               case AuthenticationResponse::FAIL:
+                                       // Hope it's not a PreAuthenticationProvider that failed...
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Authentication failed: ' . $res->message->plain() );
+                                       return false;
+                               default:
+                                       throw new BadMethodCallException(
+                                               'AuthManager returned a response unsupported by ' . __METHOD__
+                                       );
+                       }
+               }
        }
 
        /**
         * Check if the given clear-text password matches the temporary password
         * sent by e-mail for password reset operations.
         *
-        * @deprecated since 1.27. AuthManager is coming.
+        * @deprecated since 1.27, use AuthManager instead
         * @param string $plaintext
         * @return bool True if matches, false otherwise
         */
        public function checkTemporaryPassword( $plaintext ) {
-               global $wgNewPasswordExpiry;
+               global $wgNewPasswordExpiry, $wgDisableAuthManager;
 
-               $this->load();
+               if ( $wgDisableAuthManager ) {
+                       $this->load();
 
-               $passwordFactory = new PasswordFactory();
-               $passwordFactory->init( RequestContext::getMain()->getConfig() );
-               $db = ( $this->queryFlagsUsed & self::READ_LATEST )
-                       ? wfGetDB( DB_MASTER )
-                       : wfGetDB( DB_SLAVE );
+                       $passwordFactory = new PasswordFactory();
+                       $passwordFactory->init( RequestContext::getMain()->getConfig() );
+                       $db = ( $this->queryFlagsUsed & self::READ_LATEST )
+                               ? wfGetDB( DB_MASTER )
+                               : wfGetDB( DB_SLAVE );
 
-               $row = $db->selectRow(
-                       'user',
-                       [ 'user_newpassword', 'user_newpass_time' ],
-                       [ 'user_id' => $this->getId() ],
-                       __METHOD__
-               );
-               try {
-                       $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
-               } catch ( PasswordError $e ) {
-                       wfDebug( 'Invalid password hash found in database.' );
-                       $newPassword = PasswordFactory::newInvalidPassword();
-               }
+                       $row = $db->selectRow(
+                               'user',
+                               [ 'user_newpassword', 'user_newpass_time' ],
+                               [ 'user_id' => $this->getId() ],
+                               __METHOD__
+                       );
+                       try {
+                               $newPassword = $passwordFactory->newFromCiphertext( $row->user_newpassword );
+                       } catch ( PasswordError $e ) {
+                               wfDebug( 'Invalid password hash found in database.' );
+                               $newPassword = PasswordFactory::newInvalidPassword();
+                       }
 
-               if ( $newPassword->equals( $plaintext ) ) {
-                       if ( is_null( $row->user_newpass_time ) ) {
-                               return true;
+                       if ( $newPassword->equals( $plaintext ) ) {
+                               if ( is_null( $row->user_newpass_time ) ) {
+                                       return true;
+                               }
+                               $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
+                               return ( time() < $expiry );
+                       } else {
+                               return false;
                        }
-                       $expiry = wfTimestamp( TS_UNIX, $row->user_newpass_time ) + $wgNewPasswordExpiry;
-                       return ( time() < $expiry );
                } else {
-                       return false;
+                       // Can't check the temporary password individually.
+                       return $this->checkPassword( $plaintext );
                }
        }
 
@@ -5089,6 +5246,7 @@ class User implements IDBAccessObject {
         * Add a newuser log entry for this user.
         * Before 1.19 the return value was always true.
         *
+        * @deprecated since 1.27, AuthManager handles logging
         * @param string|bool $action Account creation type.
         *   - String, one of the following values:
         *     - 'create' for an anonymous user creating an account for himself.
@@ -5101,14 +5259,13 @@ class User implements IDBAccessObject {
         *     - true will be converted to 'byemail'
         *     - false will be converted to 'create' if this object is the same as
         *       $wgUser and to 'create2' otherwise
-        *
         * @param string $reason User supplied reason
-        *
-        * @return int|bool True if not $wgNewUserLog; otherwise ID of log item or 0 on failure
+        * @return int|bool True if not $wgNewUserLog or not $wgDisableAuthManager;
+        *   otherwise ID of log item or 0 on failure
         */
        public function addNewUserLogEntry( $action = false, $reason = '' ) {
-               global $wgUser, $wgNewUserLog;
-               if ( empty( $wgNewUserLog ) ) {
+               global $wgUser, $wgNewUserLog, $wgDisableAuthManager;
+               if ( !$wgDisableAuthManager || empty( $wgNewUserLog ) ) {
                        return true; // disabled
                }
 
@@ -5149,6 +5306,7 @@ class User implements IDBAccessObject {
         * Used by things like CentralAuth and perhaps other authplugins.
         * Consider calling addNewUserLogEntry() directly instead.
         *
+        * @deprecated since 1.27, AuthManager handles logging
         * @return bool
         */
        public function addNewUserLogEntryAutoCreate() {