<?php
use MediaWiki\MediaWikiServices;
+use MediaWiki\Session\BotPasswordSessionProvider;
+use MediaWiki\Session\SessionManager;
use Wikimedia\TestingAccessWrapper;
/**
* @covers ApiLogin
*/
class ApiLoginTest extends ApiTestCase {
+ public function setUp() {
+ parent::setUp();
+
+ $this->tablesUsed[] = 'bot_passwords';
+ }
+
+ public static function provideEnableBotPasswords() {
+ return [
+ 'Bot passwords enabled' => [ true ],
+ 'Bot passwords disabled' => [ false ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideEnableBotPasswords
+ */
+ public function testExtendedDescription( $enableBotPasswords ) {
+ $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+ $ret = $this->doApiRequest( [
+ 'action' => 'paraminfo',
+ 'modules' => 'login',
+ 'helpformat' => 'raw',
+ ] );
+ $this->assertSame(
+ 'apihelp-login-extended-description' . ( $enableBotPasswords ? '' : '-nobotpasswords' ),
+ $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
+ );
+ }
/**
* Test result of attempted login with an empty username
*/
- public function testApiLoginNoName() {
+ public function testNoName() {
$session = [
'wsTokenSecrets' => [ 'login' => 'foobar' ],
];
- $data = $this->doApiRequest( [ 'action' => 'login',
- 'lgname' => '', 'lgpassword' => self::$users['sysop']->getPassword(),
- 'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) )
+ $ret = $this->doApiRequest( [
+ 'action' => 'login',
+ 'lgname' => '',
+ 'lgpassword' => self::$users['sysop']->getPassword(),
+ 'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) ),
], $session );
- $this->assertEquals( 'Failed', $data[0]['login']['result'] );
+ $this->assertSame( 'Failed', $ret[0]['login']['result'] );
}
- public function testApiLoginBadPass() {
- global $wgServer;
+ /**
+ * @dataProvider provideEnableBotPasswords
+ */
+ public function testDeprecatedUserLogin( $enableBotPasswords ) {
+ $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
- $user = self::$users['sysop'];
- $userName = $user->getUser()->getName();
- $user->getUser()->logout();
+ $user = $this->getTestUser();
- if ( !isset( $wgServer ) ) {
- $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
- }
$ret = $this->doApiRequest( [
- "action" => "login",
- "lgname" => $userName,
- "lgpassword" => "bad",
+ 'action' => 'login',
+ 'lgname' => $user->getUser()->getName(),
] );
- $result = $ret[0];
-
- $this->assertNotInternalType( "bool", $result );
- $a = $result["login"]["result"];
- $this->assertEquals( "NeedToken", $a );
+ $this->assertSame(
+ [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+ 'apiwarn-deprecation-login-token' )->text() ) ],
+ $ret[0]['warnings']['login']
+ );
+ $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
- $token = $result["login"]["token"];
+ $ret = $this->doApiRequest( [
+ 'action' => 'login',
+ 'lgtoken' => $ret[0]['login']['token'],
+ 'lgname' => $user->getUser()->getName(),
+ 'lgpassword' => $user->getPassword(),
+ ], $ret[2] );
- $ret = $this->doApiRequest(
+ $this->assertSame(
+ [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+ 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
+ ->text() ) ],
+ $ret[0]['warnings']['login']
+ );
+ $this->assertSame(
[
- "action" => "login",
- "lgtoken" => $token,
- "lgname" => $userName,
- "lgpassword" => "badnowayinhell",
+ 'result' => 'Success',
+ 'lguserid' => $user->getUser()->getId(),
+ 'lgusername' => $user->getUser()->getName(),
],
- $ret[2]
+ $ret[0]['login']
);
+ }
- $result = $ret[0];
+ /**
+ * Attempts to log in with the given name and password, retrieves the returned token, and makes
+ * a second API request to actually log in with the token.
+ *
+ * @param string $name
+ * @param string $password
+ * @param array $params To pass to second request
+ * @return array Result of second doApiRequest
+ */
+ private function doUserLogin( $name, $password, array $params = [] ) {
+ $ret = $this->doApiRequest( [
+ 'action' => 'query',
+ 'meta' => 'tokens',
+ 'type' => 'login',
+ ] );
- $this->assertNotInternalType( "bool", $result );
- $a = $result["login"]["result"];
+ $this->assertArrayNotHasKey( 'warnings', $ret );
- $this->assertEquals( 'Failed', $a );
+ return $this->doApiRequest( array_merge(
+ [
+ 'action' => 'login',
+ 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
+ 'lgname' => $name,
+ 'lgpassword' => $password,
+ ], $params
+ ), $ret[2] );
}
- public function testApiLoginGoodPass() {
- global $wgServer;
+ public function testBadToken() {
+ $user = self::$users['sysop'];
+ $userName = $user->getUser()->getName();
+ $password = $user->getPassword();
+ $user->getUser()->logout();
+
+ $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
- if ( !isset( $wgServer ) ) {
- $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
- }
+ $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
+ }
+ public function testBadPass() {
$user = self::$users['sysop'];
$userName = $user->getUser()->getName();
- $password = $user->getPassword();
$user->getUser()->logout();
- $ret = $this->doApiRequest( [
- "action" => "login",
- "lgname" => $userName,
- "lgpassword" => $password,
- ]
- );
+ $ret = $this->doUserLogin( $userName, 'bad' );
- $result = $ret[0];
- $this->assertNotInternalType( "bool", $result );
- $this->assertNotInternalType( "null", $result["login"] );
+ $this->assertSame( 'Failed', $ret[0]['login']['result'] );
+ }
- $a = $result["login"]["result"];
- $this->assertEquals( "NeedToken", $a );
- $token = $result["login"]["token"];
+ /**
+ * @dataProvider provideEnableBotPasswords
+ */
+ public function testGoodPass( $enableBotPasswords ) {
+ $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
- $ret = $this->doApiRequest(
- [
- "action" => "login",
- "lgtoken" => $token,
- "lgname" => $userName,
- "lgpassword" => $password,
- ],
- $ret[2]
+ $user = self::$users['sysop'];
+ $userName = $user->getUser()->getName();
+ $password = $user->getPassword();
+ $user->getUser()->logout();
+
+ $ret = $this->doUserLogin( $userName, $password );
+
+ $this->assertSame( 'Success', $ret[0]['login']['result'] );
+ $this->assertSame(
+ [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
+ 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
+ text() ) ],
+ $ret[0]['warnings']['login']
+ );
+ }
+
+ /**
+ * @dataProvider provideEnableBotPasswords
+ */
+ public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
+ $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
+
+ $mockProvider = $this->createMock(
+ MediaWiki\Auth\AbstractSecondaryAuthenticationProvider::class );
+ $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
+ MediaWiki\Auth\AuthenticationResponse::newUI(
+ [ new MediaWiki\Auth\UsernameAuthenticationRequest ],
+ // Slightly silly message here
+ wfMessage( 'mainpage' )
+ )
);
+ $mockProvider->method( 'getAuthenticationRequests' )
+ ->willReturn( [] );
+
+ $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
+ 'secondaryauth' => [ [
+ 'factory' => function () use ( $mockProvider ) {
+ return $mockProvider;
+ },
+ ] ],
+ ] );
- $result = $ret[0];
+ $user = self::$users['sysop'];
+ $userName = $user->getUser()->getName();
+ $password = $user->getPassword();
+ $user->getUser()->logout();
- $this->assertNotInternalType( "bool", $result );
- $a = $result["login"]["result"];
+ $ret = $this->doUserLogin( $userName, $password );
- $this->assertEquals( "Success", $a );
+ $this->assertSame( [ 'login' => [
+ 'result' => 'Aborted',
+ 'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
+ 'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
+ ] ], $ret[0] );
}
/**
+ * @todo Should this test just be deleted?
* @group Broken
*/
- public function testApiLoginGotCookie() {
+ public function testGotCookie() {
$this->markTestIncomplete( "The server can't do external HTTP requests, "
. "and the internal one won't give cookies" );
global $wgServer, $wgScriptPath;
- if ( !isset( $wgServer ) ) {
- $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
- }
$user = self::$users['sysop'];
$userName = $user->getUser()->getName();
$password = $user->getPassword();
- $req = MWHttpRequest::factory( self::$apiUrl . "?action=login&format=xml",
- [ "method" => "POST",
- "postData" => [
- "lgname" => $userName,
- "lgpassword" => $password
- ]
+ $req = MWHttpRequest::factory(
+ self::$apiUrl . '?action=login&format=json',
+ [
+ 'method' => 'POST',
+ 'postData' => [
+ 'lgname' => $userName,
+ 'lgpassword' => $password,
+ ],
],
__METHOD__
);
$req->execute();
- libxml_use_internal_errors( true );
- $sxe = simplexml_load_string( $req->getContent() );
- $this->assertNotInternalType( "bool", $sxe );
- $this->assertThat( $sxe, $this->isInstanceOf( SimpleXMLElement::class ) );
- $this->assertNotInternalType( "null", $sxe->login[0] );
+ $content = json_decode( $req->getContent() );
- $a = $sxe->login[0]->attributes()->result[0];
- $this->assertEquals( ' result="NeedToken"', $a->asXML() );
- $token = (string)$sxe->login[0]->attributes()->token;
+ $this->assertSame( 'NeedToken', $content->login->result );
$req->setData( [
- "lgtoken" => $token,
- "lgname" => $userName,
- "lgpassword" => $password ] );
+ 'lgtoken' => $content->login->token,
+ 'lgname' => $userName,
+ 'lgpassword' => $password,
+ ] );
$req->execute();
$cj = $req->getCookieJar();
$serverName = parse_url( $wgServer, PHP_URL_HOST );
$this->assertNotEquals( false, $serverName );
$serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
- $this->assertNotEquals( '', $serializedCookie );
$this->assertRegExp(
- '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $user->userName . '; .*Token=/',
+ '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
$serializedCookie
);
}
- public function testRunLogin() {
- $user = self::$users['sysop'];
- $userName = $user->getUser()->getName();
- $password = $user->getPassword();
-
- $data = $this->doApiRequest( [
- 'action' => 'login',
- 'lgname' => $userName,
- 'lgpassword' => $password ] );
-
- $this->assertArrayHasKey( "login", $data[0] );
- $this->assertArrayHasKey( "result", $data[0]['login'] );
- $this->assertEquals( "NeedToken", $data[0]['login']['result'] );
- $token = $data[0]['login']['token'];
-
- $data = $this->doApiRequest( [
- 'action' => 'login',
- "lgtoken" => $token,
- "lgname" => $userName,
- "lgpassword" => $password ], $data[2] );
-
- $this->assertArrayHasKey( "login", $data[0] );
- $this->assertArrayHasKey( "result", $data[0]['login'] );
- $this->assertEquals( "Success", $data[0]['login']['result'] );
- }
-
- public function testBotPassword() {
- global $wgServer, $wgSessionProviders;
-
- if ( !isset( $wgServer ) ) {
- $this->markTestIncomplete( 'This test needs $wgServer to be set in LocalSettings.php' );
- }
+ /**
+ * @return [ $username, $password ] suitable for passing to an API request for successful login
+ */
+ private function setUpForBotPassword() {
+ global $wgSessionProviders;
$this->setMwGlobals( [
+ // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
+ // with index 0
'wgSessionProviders' => array_merge( $wgSessionProviders, [
[
- 'class' => MediaWiki\Session\BotPasswordSessionProvider::class,
+ 'class' => BotPasswordSessionProvider::class,
'args' => [ [ 'priority' => 40 ] ],
- ]
+ ],
] ),
'wgEnableBotPasswords' => true,
'wgBotPasswordsDatabase' => false,
] );
// Make sure our session provider is present
- $manager = TestingAccessWrapper::newFromObject( MediaWiki\Session\SessionManager::singleton() );
- if ( !isset( $manager->sessionProviders[MediaWiki\Session\BotPasswordSessionProvider::class] ) ) {
+ $manager = TestingAccessWrapper::newFromObject( SessionManager::singleton() );
+ if ( !isset( $manager->sessionProviders[BotPasswordSessionProvider::class] ) ) {
$tmp = $manager->sessionProviders;
$manager->sessionProviders = null;
$manager->sessionProviders = $tmp + $manager->getProviders();
}
$this->assertNotNull(
- MediaWiki\Session\SessionManager::singleton()->getProvider(
- MediaWiki\Session\BotPasswordSessionProvider::class
- ),
+ SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class ),
'sanity check'
);
$user = self::$users['sysop'];
$centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
- $this->assertNotEquals( 0, $centralId, 'sanity check' );
+ $this->assertNotSame( 0, $centralId, 'sanity check' );
$password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
$passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
$lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
- $ret = $this->doApiRequest( [
- 'action' => 'login',
- 'lgname' => $lgName,
- 'lgpassword' => $password,
- ] );
+ return [ $lgName, $password ];
+ }
- $result = $ret[0];
- $this->assertNotInternalType( 'bool', $result );
- $this->assertNotInternalType( 'null', $result['login'] );
+ public function testBotPassword() {
+ $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
- $a = $result['login']['result'];
- $this->assertEquals( 'NeedToken', $a );
- $token = $result['login']['token'];
+ $this->assertSame( 'Success', $ret[0]['login']['result'] );
+ }
- $ret = $this->doApiRequest( [
- 'action' => 'login',
- 'lgtoken' => $token,
- 'lgname' => $lgName,
- 'lgpassword' => $password,
- ], $ret[2] );
+ public function testBotPasswordThrottled() {
+ global $wgPasswordAttemptThrottle;
+
+ $this->setGroupPermissions( 'sysop', 'noratelimit', false );
+ $this->setMwGlobals( 'wgMainCacheType', 'hash' );
- $result = $ret[0];
- $this->assertNotInternalType( 'bool', $result );
- $a = $result['login']['result'];
+ list( $name, $password ) = $this->setUpForBotPassword();
+
+ for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++ ) {
+ $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
+ }
- $this->assertEquals( 'Success', $a );
+ $ret = $this->doUserLogin( $name, $password );
+
+ $this->assertSame( [
+ 'result' => 'Failed',
+ 'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
+ durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
+ ], $ret[0]['login'] );
}
- public function testLoginWithNoSameOriginSecurity() {
+ public function testBotPasswordLocked() {
+ $this->setTemporaryHook( 'UserIsLocked', function ( User $unused, &$isLocked ) {
+ $isLocked = true;
+ return true;
+ } );
+
+ $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
+
+ $this->assertSame( [
+ 'result' => 'Failed',
+ 'reason' => wfMessage( 'botpasswords-locked' )->text(),
+ ], $ret[0]['login'] );
+ }
+
+ public function testNoSameOriginSecurity() {
$this->setTemporaryHook( 'RequestHasSameOriginSecurity',
function () {
return false;
}
);
- $result = $this->doApiRequest( [
+ $ret = $this->doApiRequest( [
'action' => 'login',
+ 'errorformat' => 'plaintext',
] )[0]['login'];
$this->assertSame( [
'result' => 'Aborted',
- 'reason' => 'Cannot log in when the same-origin policy is not applied.',
- ], $result );
+ 'reason' => [
+ 'code' => 'api-login-fail-sameorigin',
+ 'text' => 'Cannot log in when the same-origin policy is not applied.',
+ ],
+ ], $ret );
}
}