/** * @dataProvider provideSave * @param string|null $password */ public function testSave($password) { $passwordFactory = new \PasswordFactory(); $passwordFactory->init(\RequestContext::getMain()->getConfig()); $bp = BotPassword::newUnsaved(['centralId' => 42, 'appId' => 'TestSave', 'restrictions' => MWRestrictions::newFromJson('{"IPAddresses":["127.0.0.0/8"]}'), 'grants' => ['test']]); $this->assertFalse($bp->isSaved(), 'sanity check'); $this->assertNull(BotPassword::newFromCentralId(42, 'TestSave', BotPassword::READ_LATEST), 'sanity check'); $passwordHash = $password ? $passwordFactory->newFromPlaintext($password) : null; $this->assertFalse($bp->save('update', $passwordHash)); $this->assertTrue($bp->save('insert', $passwordHash)); $bp2 = BotPassword::newFromCentralId(42, 'TestSave', BotPassword::READ_LATEST); $this->assertInstanceOf('BotPassword', $bp2); $this->assertEquals($bp->getUserCentralId(), $bp2->getUserCentralId()); $this->assertEquals($bp->getAppId(), $bp2->getAppId()); $this->assertEquals($bp->getToken(), $bp2->getToken()); $this->assertEquals($bp->getRestrictions(), $bp2->getRestrictions()); $this->assertEquals($bp->getGrants(), $bp2->getGrants()); $pw = TestingAccessWrapper::newFromObject($bp)->getPassword(); if ($password === null) { $this->assertInstanceOf('InvalidPassword', $pw); } else { $this->assertTrue($pw->equals($password)); } $token = $bp->getToken(); $this->assertFalse($bp->save('insert')); $this->assertTrue($bp->save('update')); $this->assertNotEquals($token, $bp->getToken()); $bp2 = BotPassword::newFromCentralId(42, 'TestSave', BotPassword::READ_LATEST); $this->assertInstanceOf('BotPassword', $bp2); $this->assertEquals($bp->getToken(), $bp2->getToken()); $pw = TestingAccessWrapper::newFromObject($bp)->getPassword(); if ($password === null) { $this->assertInstanceOf('InvalidPassword', $pw); } else { $this->assertTrue($pw->equals($password)); } $passwordHash = $passwordFactory->newFromPlaintext('XXX'); $token = $bp->getToken(); $this->assertTrue($bp->save('update', $passwordHash)); $this->assertNotEquals($token, $bp->getToken()); $pw = TestingAccessWrapper::newFromObject($bp)->getPassword(); $this->assertTrue($pw->equals('XXX')); $this->assertTrue($bp->delete()); $this->assertFalse($bp->isSaved()); $this->assertNull(BotPassword::newFromCentralId(42, 'TestSave', BotPassword::READ_LATEST)); $this->assertFalse($bp->save('foobar')); }
public function onSuccess() { $out = $this->getOutput(); $username = $this->getUser()->getName(); switch ($this->operation) { case 'insert': $out->setPageTitle($this->msg('botpasswords-created-title')->text()); $out->addWikiMsg('botpasswords-created-body', $this->par, $username); break; case 'update': $out->setPageTitle($this->msg('botpasswords-updated-title')->text()); $out->addWikiMsg('botpasswords-updated-body', $this->par, $username); break; case 'delete': $out->setPageTitle($this->msg('botpasswords-deleted-title')->text()); $out->addWikiMsg('botpasswords-deleted-body', $this->par, $username); $this->password = null; break; } if ($this->password !== null) { $sep = BotPassword::getSeparator(); $out->addWikiMsg('botpasswords-newpassword', htmlspecialchars($username . $sep . $this->par), htmlspecialchars($this->password)); $this->password = null; } $out->addReturnTo($this->getPageTitle()); }
public function testBotPassword() { global $wgServer, $wgSessionProviders; if (!isset($wgServer)) { $this->markTestIncomplete('This test needs $wgServer to be set in LocalSettings.php'); } $this->setMwGlobals(array('wgSessionProviders' => array_merge($wgSessionProviders, array(array('class' => 'MediaWiki\\Session\\BotPasswordSessionProvider', 'args' => array(array('priority' => 40))))), 'wgEnableBotPasswords' => true, 'wgBotPasswordsDatabase' => false, 'wgCentralIdLookupProvider' => 'local', 'wgGrantPermissions' => array('test' => array('read' => true)))); // Make sure our session provider is present $manager = TestingAccessWrapper::newFromObject(MediaWiki\Session\SessionManager::singleton()); if (!isset($manager->sessionProviders['MediaWiki\\Session\\BotPasswordSessionProvider'])) { $tmp = $manager->sessionProviders; $manager->sessionProviders = null; $manager->sessionProviders = $tmp + $manager->getProviders(); } $this->assertNotNull(MediaWiki\Session\SessionManager::singleton()->getProvider('MediaWiki\\Session\\BotPasswordSessionProvider'), 'sanity check'); $user = self::$users['sysop']; $centralId = CentralIdLookup::factory()->centralIdFromLocalUser($user->getUser()); $this->assertNotEquals(0, $centralId, 'sanity check'); $passwordFactory = new PasswordFactory(); $passwordFactory->init(RequestContext::getMain()->getConfig()); // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only $passwordFactory->setDefaultType('A'); $pwhash = $passwordFactory->newFromPlaintext('foobaz'); $dbw = wfGetDB(DB_MASTER); $dbw->insert('bot_passwords', array('bp_user' => $centralId, 'bp_app_id' => 'foo', 'bp_password' => $pwhash->toString(), 'bp_token' => '', 'bp_restrictions' => MWRestrictions::newDefault()->toJson(), 'bp_grants' => '["test"]'), __METHOD__); $lgName = $user->username . BotPassword::getSeparator() . 'foo'; $ret = $this->doApiRequest(array('action' => 'login', 'lgname' => $lgName, 'lgpassword' => 'foobaz')); $result = $ret[0]; $this->assertNotInternalType('bool', $result); $this->assertNotInternalType('null', $result['login']); $a = $result['login']['result']; $this->assertEquals('NeedToken', $a); $token = $result['login']['token']; $ret = $this->doApiRequest(array('action' => 'login', 'lgtoken' => $token, 'lgname' => $lgName, 'lgpassword' => 'foobaz'), $ret[2]); $result = $ret[0]; $this->assertNotInternalType('bool', $result); $a = $result['login']['result']; $this->assertEquals('Success', $a); }
/** * Executes the log-in attempt using the parameters passed. If * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { // If we're in a mode that breaks the same-origin policy, no tokens can // be obtained if ($this->lacksSameOriginSecurity()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when the same-origin policy is not applied']); return; } try { $this->requirePostedParameters(['password', 'token']); } catch (UsageException $ex) { // Make this a warning for now, upgrade to an error in 1.29. $this->setWarning($ex->getMessage()); $this->logFeatureUsage('login-params-in-query-string'); } $params = $this->extractRequestParams(); $result = []; // Make sure session is persisted $session = MediaWiki\Session\SessionManager::getGlobalSession(); $session->persist(); // Make sure it's possible to log in if (!$session->canSetUser()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when using ' . $session->getProvider()->describe(Language::factory('en'))]); return; } $authRes = false; $context = new DerivativeContext($this->getContext()); $loginType = 'N/A'; // Check login token $token = $session->getToken('', 'login'); if ($token->wasNew() || !$params['token']) { $authRes = 'NeedToken'; } elseif (!$token->match($params['token'])) { $authRes = 'WrongToken'; } // Try bot passwords if ($authRes === false && $this->getConfig()->get('EnableBotPasswords') && ($botLoginData = BotPassword::canonicalizeLoginData($params['name'], $params['password']))) { $status = BotPassword::login($botLoginData[0], $botLoginData[1], $this->getRequest()); if ($status->isOK()) { $session = $status->getValue(); $authRes = 'Success'; $loginType = 'BotPassword'; } elseif (!$botLoginData[2]) { $authRes = 'Failed'; $message = $status->getMessage(); LoggerFactory::getInstance('authentication')->info('BotPassword login failed: ' . $status->getWikiText(false, false, 'en')); } } if ($authRes === false) { // Simplified AuthManager login, for backwards compatibility $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN, $this->getUser()), ['username' => $params['name'], 'password' => $params['password'], 'domain' => $params['domain'], 'rememberMe' => true]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: if ($this->getConfig()->get('EnableBotPasswords')) { $warn = 'Main-account login via action=login is deprecated and may stop working ' . 'without warning.'; $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].'; $warn .= ' To safely continue using main-account login, see action=clientlogin.'; } else { $warn = 'Login via action=login is deprecated and may stop working without warning.'; $warn .= ' To safely log in, see action=clientlogin.'; } $this->setWarning($warn); $authRes = 'Success'; $loginType = 'AuthManager'; break; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... $authRes = 'Failed'; $message = $res->message; \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $message->inLanguage('en')->plain()); break; default: \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed due to unsupported response type: ' . $res->status, $this->getAuthenticationResponseLogData($res)); $authRes = 'Aborted'; break; } } $result['result'] = $authRes; switch ($authRes) { case 'Success': $user = $session->getUser(); ApiQueryInfo::resetTokenCache(); // Deprecated hook $injected_html = ''; Hooks::run('UserLoginComplete', [&$user, &$injected_html, true]); $result['lguserid'] = intval($user->getId()); $result['lgusername'] = $user->getName(); break; case 'NeedToken': $result['token'] = $token->toString(); $this->setWarning('Fetching a token via action=login is deprecated. ' . 'Use action=query&meta=tokens&type=login instead.'); $this->logFeatureUsage('action=login&!lgtoken'); break; case 'WrongToken': break; case 'Failed': $result['reason'] = $message->useDatabase('false')->inLanguage('en')->text(); break; case 'Aborted': $result['reason'] = 'Authentication requires user interaction, ' . 'which is not supported by action=login.'; if ($this->getConfig()->get('EnableBotPasswords')) { $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].'; $result['reason'] .= ' To continue using main-account login, see action=clientlogin.'; } else { $result['reason'] .= ' To log in, see action=clientlogin.'; } break; default: ApiBase::dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } $this->getResult()->addValue(null, 'login', $result); if ($loginType === 'LoginForm' && isset(LoginForm::$statusCodes[$authRes])) { $authRes = LoginForm::$statusCodes[$authRes]; } LoggerFactory::getInstance('authevents')->info('Login attempt', ['event' => 'login', 'successful' => $authRes === 'Success', 'loginType' => $loginType, 'status' => $authRes]); }
public function testCheckSessionInfo() { $logger = new \TestLogger(true, function ($m) { return preg_replace('/^Session \\[\\d+\\][a-zA-Z0-9_\\\\]+<(?:null|anon|[+-]:\\d+:\\w+)>\\w+: /', 'Session X: ', $m); }); $provider = $this->getProvider(); $provider->setLogger($logger); $user = \User::newFromName('UTSysop'); $request = $this->getMock('FauxRequest', array('getIP')); $request->expects($this->any())->method('getIP')->will($this->returnValue('127.0.0.1')); $bp = \BotPassword::newFromUser($user, 'BotPasswordSessionProvider'); $data = array('provider' => $provider, 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'userInfo' => UserInfo::newFromUser($user, true), 'persisted' => false, 'metadata' => array('centralId' => $bp->getUserCentralId(), 'appId' => $bp->getAppId(), 'token' => $bp->getToken())); $dataMD = $data['metadata']; foreach (array_keys($data['metadata']) as $key) { $data['metadata'] = $dataMD; unset($data['metadata'][$key]); $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame(array(array(LogLevel::INFO, "Session X: Missing metadata: {$key}")), $logger->getBuffer()); $logger->clearBuffer(); } $data['metadata'] = $dataMD; $data['metadata']['appId'] = 'Foobar'; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame(array(array(LogLevel::INFO, "Session X: No BotPassword for {$bp->getUserCentralId()} Foobar")), $logger->getBuffer()); $logger->clearBuffer(); $data['metadata'] = $dataMD; $data['metadata']['token'] = 'Foobar'; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame(array(array(LogLevel::INFO, 'Session X: BotPassword token check failed')), $logger->getBuffer()); $logger->clearBuffer(); $request2 = $this->getMock('FauxRequest', array('getIP')); $request2->expects($this->any())->method('getIP')->will($this->returnValue('10.0.0.1')); $data['metadata'] = $dataMD; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request2, $metadata)); $this->assertSame(array(array(LogLevel::INFO, 'Session X: Restrictions check failed')), $logger->getBuffer()); $logger->clearBuffer(); $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertTrue($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame(array(), $logger->getBuffer()); $this->assertEquals($dataMD + array('rights' => array('read')), $metadata); }
/** * Executes the log-in attempt using the parameters passed. If * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { // If we're in a mode that breaks the same-origin policy, no tokens can // be obtained if ($this->lacksSameOriginSecurity()) { $this->getResult()->addValue(null, 'login', array('result' => 'Aborted', 'reason' => 'Cannot log in when the same-origin policy is not applied')); return; } $params = $this->extractRequestParams(); $result = array(); // Make sure session is persisted $session = MediaWiki\Session\SessionManager::getGlobalSession(); $session->persist(); // Make sure it's possible to log in if (!$session->canSetUser()) { $this->getResult()->addValue(null, 'login', array('result' => 'Aborted', 'reason' => 'Cannot log in when using ' . $session->getProvider()->describe(Language::factory('en')))); return; } $authRes = false; $context = new DerivativeContext($this->getContext()); $loginType = 'N/A'; // Check login token $token = LoginForm::getLoginToken(); if (!$token) { LoginForm::setLoginToken(); $authRes = LoginForm::NEED_TOKEN; } elseif (!$params['token']) { $authRes = LoginForm::NEED_TOKEN; } elseif ($token !== $params['token']) { $authRes = LoginForm::WRONG_TOKEN; } // Try bot passwords if ($authRes === false && $this->getConfig()->get('EnableBotPasswords') && strpos($params['name'], BotPassword::getSeparator()) !== false) { $status = BotPassword::login($params['name'], $params['password'], $this->getRequest()); if ($status->isOk()) { $session = $status->getValue(); $authRes = LoginForm::SUCCESS; $loginType = 'BotPassword'; } else { LoggerFactory::getInstance('authmanager')->info('BotPassword login failed: ' . $status->getWikiText()); } } // Normal login if ($authRes === false) { $context->setRequest(new DerivativeRequest($this->getContext()->getRequest(), array('wpName' => $params['name'], 'wpPassword' => $params['password'], 'wpDomain' => $params['domain'], 'wpLoginToken' => $params['token'], 'wpRemember' => ''))); $loginForm = new LoginForm(); $loginForm->setContext($context); $authRes = $loginForm->authenticateUserData(); $loginType = 'LoginForm'; } switch ($authRes) { case LoginForm::SUCCESS: $user = $context->getUser(); $this->getContext()->setUser($user); $user->setCookies($this->getRequest(), null, true); ApiQueryInfo::resetTokenCache(); // Run hooks. // @todo FIXME: Split back and frontend from this hook. // @todo FIXME: This hook should be placed in the backend $injected_html = ''; Hooks::run('UserLoginComplete', array(&$user, &$injected_html)); $result['result'] = 'Success'; $result['lguserid'] = intval($user->getId()); $result['lgusername'] = $user->getName(); // @todo: These are deprecated, and should be removed at some // point (1.28 at the earliest, and see T121527). They were ok // when the core cookie-based login was the only thing, but // CentralAuth broke that a while back and // SessionManager/AuthManager are *really* going to break it. $result['lgtoken'] = $user->getToken(); $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case LoginForm::NEED_TOKEN: $result['result'] = 'NeedToken'; $result['token'] = LoginForm::getLoginToken(); // @todo: See above about deprecation $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case LoginForm::WRONG_TOKEN: $result['result'] = 'WrongToken'; break; case LoginForm::NO_NAME: $result['result'] = 'NoName'; break; case LoginForm::ILLEGAL: $result['result'] = 'Illegal'; break; case LoginForm::WRONG_PLUGIN_PASS: $result['result'] = 'WrongPluginPass'; break; case LoginForm::NOT_EXISTS: $result['result'] = 'NotExists'; break; // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. case LoginForm::RESET_PASS: case LoginForm::WRONG_PASS: $result['result'] = 'WrongPass'; break; case LoginForm::EMPTY_PASS: $result['result'] = 'EmptyPass'; break; case LoginForm::CREATE_BLOCKED: $result['result'] = 'CreateBlocked'; $result['details'] = 'Your IP address is blocked from account creation'; $block = $context->getUser()->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::THROTTLED: $result['result'] = 'Throttled'; $throttle = $this->getConfig()->get('PasswordAttemptThrottle'); $result['wait'] = intval($throttle['seconds']); break; case LoginForm::USER_BLOCKED: $result['result'] = 'Blocked'; $block = User::newFromName($params['name'])->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::ABORTED: $result['result'] = 'Aborted'; $result['reason'] = $loginForm->mAbortLoginErrorMsg; break; default: ApiBase::dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } $this->getResult()->addValue(null, 'login', $result); LoggerFactory::getInstance('authmanager')->info('Login attempt', array('event' => 'login', 'successful' => $authRes === LoginForm::SUCCESS, 'loginType' => $loginType, 'status' => LoginForm::$statusCodes[$authRes])); }
/** * There are two ways to login with a bot password: "******", "password" and * "username", "appId@password". Transform it so it is always in the first form. * Returns [bot username, bot password, could be normal password?] where the last one is a flag * meaning this could either be a bot password or a normal password, it cannot be decided for * certain (although in such cases it almost always will be a bot password). * If this cannot be a bot password login just return false. * @param string $username * @param string $password * @return array|false */ public static function canonicalizeLoginData($username, $password) { $sep = BotPassword::getSeparator(); // the strlen check helps minimize the password information obtainable from timing if (strlen($password) >= 32 && strpos($username, $sep) !== false) { // the separator is not valid in new usernames but might appear in legacy ones if (preg_match('/^[0-9a-w]{32,}$/', $password)) { return [$username, $password, true]; } } elseif (strlen($password) > 32 && strpos($password, $sep) !== false) { $segments = explode($sep, $password); $password = array_pop($segments); $appId = implode($sep, $segments); if (preg_match('/^[0-9a-w]{32,}$/', $password)) { return [$username . $sep . $appId, $password, true]; } } return false; }
/** * Change authentication data (e.g. passwords) * * If $req was returned for AuthManager::ACTION_CHANGE, using $req should * result in a successful login in the future. * * If $req was returned for AuthManager::ACTION_REMOVE, using $req should * no longer result in a successful login. * * @param AuthenticationRequest $req */ public function changeAuthenticationData(AuthenticationRequest $req) { $this->logger->info('Changing authentication data for {user} class {what}', ['user' => is_string($req->username) ? $req->username : '******', 'what' => get_class($req)]); $this->callMethodOnProviders(6, 'providerChangeAuthenticationData', [$req]); // When the main account's authentication data is changed, invalidate // all BotPasswords too. \BotPassword::invalidateAllPasswordsForUser($req->username); }
/** * Actually set the password and such * @since 1.27 cannot set a password for a user not in the database * @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. */ 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.'); } $passwordFactory = new PasswordFactory(); $passwordFactory->init(RequestContext::getMain()->getConfig()); $dbw = wfGetDB(DB_MASTER); $dbw->update('user', array('user_password' => $passwordFactory->newFromPlaintext($str)->toString(), 'user_newpassword' => PasswordFactory::newInvalidPassword()->toString(), 'user_newpass_time' => $dbw->timestampOrNull(null)), array('user_id' => $id), __METHOD__); // When the main password is changed, invalidate all bot passwords too BotPassword::invalidateAllPasswordsForUser($this->getName()); }
public function testCheckSessionInfo() { $logger = new \TestLogger(true); $provider = $this->getProvider(); $provider->setLogger($logger); $user = static::getTestSysop()->getUser(); $request = $this->getMock('FauxRequest', ['getIP']); $request->expects($this->any())->method('getIP')->will($this->returnValue('127.0.0.1')); $bp = \BotPassword::newFromUser($user, 'BotPasswordSessionProvider'); $data = ['provider' => $provider, 'id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'userInfo' => UserInfo::newFromUser($user, true), 'persisted' => false, 'metadata' => ['centralId' => $bp->getUserCentralId(), 'appId' => $bp->getAppId(), 'token' => $bp->getToken()]]; $dataMD = $data['metadata']; foreach (array_keys($data['metadata']) as $key) { $data['metadata'] = $dataMD; unset($data['metadata'][$key]); $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame([[LogLevel::INFO, 'Session "{session}": Missing metadata: {missing}']], $logger->getBuffer()); $logger->clearBuffer(); } $data['metadata'] = $dataMD; $data['metadata']['appId'] = 'Foobar'; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame([[LogLevel::INFO, 'Session "{session}": No BotPassword for {centralId} {appId}']], $logger->getBuffer()); $logger->clearBuffer(); $data['metadata'] = $dataMD; $data['metadata']['token'] = 'Foobar'; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame([[LogLevel::INFO, 'Session "{session}": BotPassword token check failed']], $logger->getBuffer()); $logger->clearBuffer(); $request2 = $this->getMock('FauxRequest', ['getIP']); $request2->expects($this->any())->method('getIP')->will($this->returnValue('10.0.0.1')); $data['metadata'] = $dataMD; $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertFalse($provider->refreshSessionInfo($info, $request2, $metadata)); $this->assertSame([[LogLevel::INFO, 'Session "{session}": Restrictions check failed']], $logger->getBuffer()); $logger->clearBuffer(); $info = new SessionInfo(SessionInfo::MIN_PRIORITY, $data); $metadata = $info->getProviderMetadata(); $this->assertTrue($provider->refreshSessionInfo($info, $request, $metadata)); $this->assertSame([], $logger->getBuffer()); $this->assertEquals($dataMD + ['rights' => ['read']], $metadata); }
/** * Actually set the password and such * @since 1.27 cannot set a password for a user not in the database * @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) { 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); } SessionManager::singleton()->invalidateSessionsForUser($this); return true; }
/** * Executes the log-in attempt using the parameters passed. If * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { // If we're in a mode that breaks the same-origin policy, no tokens can // be obtained if ($this->lacksSameOriginSecurity()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when the same-origin policy is not applied']); return; } $params = $this->extractRequestParams(); $result = []; // Make sure session is persisted $session = MediaWiki\Session\SessionManager::getGlobalSession(); $session->persist(); // Make sure it's possible to log in if (!$session->canSetUser()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when using ' . $session->getProvider()->describe(Language::factory('en'))]); return; } $authRes = false; $context = new DerivativeContext($this->getContext()); $loginType = 'N/A'; // Check login token $token = $session->getToken('', 'login'); if ($token->wasNew() || !$params['token']) { $authRes = 'NeedToken'; } elseif (!$token->match($params['token'])) { $authRes = 'WrongToken'; } // Try bot passwords if ($authRes === false && $this->getConfig()->get('EnableBotPasswords') && strpos($params['name'], BotPassword::getSeparator()) !== false) { $status = BotPassword::login($params['name'], $params['password'], $this->getRequest()); if ($status->isOK()) { $session = $status->getValue(); $authRes = 'Success'; $loginType = 'BotPassword'; } else { $authRes = 'Failed'; $message = $status->getMessage(); LoggerFactory::getInstance('authmanager')->info('BotPassword login failed: ' . $status->getWikiText(false, false, 'en')); } } if ($authRes === false) { if ($this->getConfig()->get('DisableAuthManager')) { // Non-AuthManager login $context->setRequest(new DerivativeRequest($this->getContext()->getRequest(), ['wpName' => $params['name'], 'wpPassword' => $params['password'], 'wpDomain' => $params['domain'], 'wpLoginToken' => $params['token'], 'wpRemember' => ''])); $loginForm = new LoginForm(); $loginForm->setContext($context); $authRes = $loginForm->authenticateUserData(); $loginType = 'LoginForm'; switch ($authRes) { case LoginForm::SUCCESS: $authRes = 'Success'; break; case LoginForm::NEED_TOKEN: $authRes = 'NeedToken'; break; } } else { // Simplified AuthManager login, for backwards compatibility $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN, $this->getUser()), ['username' => $params['name'], 'password' => $params['password'], 'domain' => $params['domain'], 'rememberMe' => true]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: if ($this->getConfig()->get('EnableBotPasswords')) { $warn = 'Main-account login via action=login is deprecated and may stop working ' . 'without warning.'; $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].'; $warn .= ' To safely continue using main-account login, see action=clientlogin.'; } else { $warn = 'Login via action=login is deprecated and may stop working without warning.'; $warn .= ' To safely log in, see action=clientlogin.'; } $this->setWarning($warn); $authRes = 'Success'; $loginType = 'AuthManager'; break; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... $authRes = 'Failed'; $message = $res->message; \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $message->plain()); break; default: $authRes = 'Aborted'; break; } } } $result['result'] = $authRes; switch ($authRes) { case 'Success': if ($this->getConfig()->get('DisableAuthManager')) { $user = $context->getUser(); $this->getContext()->setUser($user); $user->setCookies($this->getRequest(), null, true); } else { $user = $session->getUser(); } ApiQueryInfo::resetTokenCache(); // Deprecated hook $injected_html = ''; Hooks::run('UserLoginComplete', [&$user, &$injected_html]); $result['lguserid'] = intval($user->getId()); $result['lgusername'] = $user->getName(); // @todo: These are deprecated, and should be removed at some // point (1.28 at the earliest, and see T121527). They were ok // when the core cookie-based login was the only thing, but // CentralAuth broke that a while back and // SessionManager/AuthManager *really* break it. $result['lgtoken'] = $user->getToken(); $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case 'NeedToken': $result['token'] = $token->toString(); $this->setWarning('Fetching a token via action=login is deprecated. ' . 'Use action=query&meta=tokens&type=login instead.'); $this->logFeatureUsage('action=login&!lgtoken'); // @todo: See above about deprecation $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case 'WrongToken': break; case 'Failed': $result['reason'] = $message->useDatabase('false')->inLanguage('en')->text(); break; case 'Aborted': $result['reason'] = 'Authentication requires user interaction, ' . 'which is not supported by action=login.'; if ($this->getConfig()->get('EnableBotPasswords')) { $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].'; $result['reason'] .= ' To continue using main-account login, see action=clientlogin.'; } else { $result['reason'] .= ' To log in, see action=clientlogin.'; } break; // Results from LoginForm for when $wgDisableAuthManager is true // Results from LoginForm for when $wgDisableAuthManager is true case LoginForm::WRONG_TOKEN: $result['result'] = 'WrongToken'; break; case LoginForm::NO_NAME: $result['result'] = 'NoName'; break; case LoginForm::ILLEGAL: $result['result'] = 'Illegal'; break; case LoginForm::WRONG_PLUGIN_PASS: $result['result'] = 'WrongPluginPass'; break; case LoginForm::NOT_EXISTS: $result['result'] = 'NotExists'; break; // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. case LoginForm::RESET_PASS: case LoginForm::WRONG_PASS: $result['result'] = 'WrongPass'; break; case LoginForm::EMPTY_PASS: $result['result'] = 'EmptyPass'; break; case LoginForm::CREATE_BLOCKED: $result['result'] = 'CreateBlocked'; $result['details'] = 'Your IP address is blocked from account creation'; $block = $context->getUser()->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::THROTTLED: $result['result'] = 'Throttled'; $result['wait'] = intval($loginForm->mThrottleWait); break; case LoginForm::USER_BLOCKED: $result['result'] = 'Blocked'; $block = User::newFromName($params['name'])->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::ABORTED: $result['result'] = 'Aborted'; $result['reason'] = $loginForm->mAbortLoginErrorMsg; break; default: ApiBase::dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } $this->getResult()->addValue(null, 'login', $result); if ($loginType === 'LoginForm' && isset(LoginForm::$statusCodes[$authRes])) { $authRes = LoginForm::$statusCodes[$authRes]; } LoggerFactory::getInstance('authmanager')->info('Login attempt', ['event' => 'login', 'successful' => $authRes === 'Success', 'loginType' => $loginType, 'status' => $authRes]); }