3 use MediaWiki\MediaWikiServices
;
4 use MediaWiki\Session\BotPasswordSessionProvider
;
5 use MediaWiki\Session\SessionManager
;
6 use Wikimedia\TestingAccessWrapper
;
15 class ApiLoginTest
extends ApiTestCase
{
16 public function setUp() {
19 $this->tablesUsed
[] = 'bot_passwords';
22 public static function provideEnableBotPasswords() {
24 'Bot passwords enabled' => [ true ],
25 'Bot passwords disabled' => [ false ],
30 * @dataProvider provideEnableBotPasswords
32 public function testExtendedDescription( $enableBotPasswords ) {
33 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
34 $ret = $this->doApiRequest( [
35 'action' => 'paraminfo',
37 'helpformat' => 'raw',
40 'apihelp-login-extended-description' . ( $enableBotPasswords ?
'' : '-nobotpasswords' ),
41 $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
46 * Test result of attempted login with an empty username
48 public function testNoName() {
50 'wsTokenSecrets' => [ 'login' => 'foobar' ],
52 $ret = $this->doApiRequest( [
55 'lgpassword' => self
::$users['sysop']->getPassword(),
56 'lgtoken' => (string)( new MediaWiki\Session\
Token( 'foobar', '' ) ),
58 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
62 * @dataProvider provideEnableBotPasswords
64 public function testDeprecatedUserLogin( $enableBotPasswords ) {
65 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
67 $user = $this->getTestUser();
69 $ret = $this->doApiRequest( [
71 'lgname' => $user->getUser()->getName(),
75 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
76 'apiwarn-deprecation-login-token' )->text() ) ],
77 $ret[0]['warnings']['login']
79 $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
81 $ret = $this->doApiRequest( [
83 'lgtoken' => $ret[0]['login']['token'],
84 'lgname' => $user->getUser()->getName(),
85 'lgpassword' => $user->getPassword(),
89 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
90 'apiwarn-deprecation-login-' . ( $enableBotPasswords ?
'' : 'no' ) . 'botpw' )
92 $ret[0]['warnings']['login']
96 'result' => 'Success',
97 'lguserid' => $user->getUser()->getId(),
98 'lgusername' => $user->getUser()->getName(),
105 * Attempts to log in with the given name and password, retrieves the returned token, and makes
106 * a second API request to actually log in with the token.
108 * @param string $name
109 * @param string $password
110 * @param array $params To pass to second request
111 * @return array Result of second doApiRequest
113 private function doUserLogin( $name, $password, array $params = [] ) {
114 $ret = $this->doApiRequest( [
120 $this->assertArrayNotHasKey( 'warnings', $ret );
122 return $this->doApiRequest( array_merge(
125 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
127 'lgpassword' => $password,
132 public function testBadToken() {
133 $user = self
::$users['sysop'];
134 $userName = $user->getUser()->getName();
135 $password = $user->getPassword();
136 $user->getUser()->logout();
138 $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
140 $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
143 public function testBadPass() {
144 $user = self
::$users['sysop'];
145 $userName = $user->getUser()->getName();
146 $user->getUser()->logout();
148 $ret = $this->doUserLogin( $userName, 'bad' );
150 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
154 * @dataProvider provideEnableBotPasswords
156 public function testGoodPass( $enableBotPasswords ) {
157 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
159 $user = self
::$users['sysop'];
160 $userName = $user->getUser()->getName();
161 $password = $user->getPassword();
162 $user->getUser()->logout();
164 $ret = $this->doUserLogin( $userName, $password );
166 $this->assertSame( 'Success', $ret[0]['login']['result'] );
168 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
169 'apiwarn-deprecation-login-' . ( $enableBotPasswords ?
'' : 'no' ) . 'botpw' )->
171 $ret[0]['warnings']['login']
176 * @dataProvider provideEnableBotPasswords
178 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
179 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
181 $mockProvider = $this->createMock(
182 MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
::class );
183 $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
184 MediaWiki\Auth\AuthenticationResponse
::newUI(
185 [ new MediaWiki\Auth\UsernameAuthenticationRequest
],
186 // Slightly silly message here
187 wfMessage( 'mainpage' )
190 $mockProvider->method( 'getAuthenticationRequests' )
193 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
194 'secondaryauth' => [ [
195 'factory' => function () use ( $mockProvider ) {
196 return $mockProvider;
201 $user = self
::$users['sysop'];
202 $userName = $user->getUser()->getName();
203 $password = $user->getPassword();
204 $user->getUser()->logout();
206 $ret = $this->doUserLogin( $userName, $password );
208 $this->assertSame( [ 'login' => [
209 'result' => 'Aborted',
210 'reason' => ApiErrorFormatter
::stripMarkup( wfMessage(
211 'api-login-fail-aborted' . ( $enableBotPasswords ?
'' : '-nobotpw' ) )->text() ),
216 * @todo Should this test just be deleted?
219 public function testGotCookie() {
220 $this->markTestIncomplete( "The server can't do external HTTP requests, "
221 . "and the internal one won't give cookies" );
223 global $wgServer, $wgScriptPath;
225 $user = self
::$users['sysop'];
226 $userName = $user->getUser()->getName();
227 $password = $user->getPassword();
229 $req = MWHttpRequest
::factory(
230 self
::$apiUrl . '?action=login&format=json',
234 'lgname' => $userName,
235 'lgpassword' => $password,
242 $content = json_decode( $req->getContent() );
244 $this->assertSame( 'NeedToken', $content->login
->result
);
247 'lgtoken' => $content->login
->token
,
248 'lgname' => $userName,
249 'lgpassword' => $password,
253 $cj = $req->getCookieJar();
254 $serverName = parse_url( $wgServer, PHP_URL_HOST
);
255 $this->assertNotEquals( false, $serverName );
256 $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
258 '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
264 * @return [ $username, $password ] suitable for passing to an API request for successful login
266 private function setUpForBotPassword() {
267 global $wgSessionProviders;
269 $this->setMwGlobals( [
270 // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
272 'wgSessionProviders' => array_merge( $wgSessionProviders, [
274 'class' => BotPasswordSessionProvider
::class,
275 'args' => [ [ 'priority' => 40 ] ],
278 'wgEnableBotPasswords' => true,
279 'wgBotPasswordsDatabase' => false,
280 'wgCentralIdLookupProvider' => 'local',
281 'wgGrantPermissions' => [
282 'test' => [ 'read' => true ],
286 // Make sure our session provider is present
287 $manager = TestingAccessWrapper
::newFromObject( SessionManager
::singleton() );
288 if ( !isset( $manager->sessionProviders
[BotPasswordSessionProvider
::class] ) ) {
289 $tmp = $manager->sessionProviders
;
290 $manager->sessionProviders
= null;
291 $manager->sessionProviders
= $tmp +
$manager->getProviders();
293 $this->assertNotNull(
294 SessionManager
::singleton()->getProvider( BotPasswordSessionProvider
::class ),
298 $user = self
::$users['sysop'];
299 $centralId = CentralIdLookup
::factory()->centralIdFromLocalUser( $user->getUser() );
300 $this->assertNotSame( 0, $centralId, 'sanity check' );
302 $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
303 $passwordFactory = MediaWikiServices
::getInstance()->getPasswordFactory();
304 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
305 $passwordHash = $passwordFactory->newFromPlaintext( $password );
307 $dbw = wfGetDB( DB_MASTER
);
311 'bp_user' => $centralId,
312 'bp_app_id' => 'foo',
313 'bp_password' => $passwordHash->toString(),
315 'bp_restrictions' => MWRestrictions
::newDefault()->toJson(),
316 'bp_grants' => '["test"]',
321 $lgName = $user->getUser()->getName() . BotPassword
::getSeparator() . 'foo';
323 return [ $lgName, $password ];
326 public function testBotPassword() {
327 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
329 $this->assertSame( 'Success', $ret[0]['login']['result'] );
332 public function testBotPasswordThrottled() {
333 global $wgPasswordAttemptThrottle;
335 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
336 $this->setMwGlobals( 'wgMainCacheType', 'hash' );
338 list( $name, $password ) = $this->setUpForBotPassword();
340 for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++
) {
341 $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
344 $ret = $this->doUserLogin( $name, $password );
347 'result' => 'Failed',
348 'reason' => ApiErrorFormatter
::stripMarkup( wfMessage( 'login-throttled' )->
349 durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
350 ], $ret[0]['login'] );
353 public function testBotPasswordLocked() {
354 $this->setTemporaryHook( 'UserIsLocked', function ( User
$unused, &$isLocked ) {
359 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
362 'result' => 'Failed',
363 'reason' => wfMessage( 'botpasswords-locked' )->text(),
364 ], $ret[0]['login'] );
367 public function testNoSameOriginSecurity() {
368 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
374 $ret = $this->doApiRequest( [
376 'errorformat' => 'plaintext',
380 'result' => 'Aborted',
382 'code' => 'api-login-fail-sameorigin',
383 'text' => 'Cannot log in when the same-origin policy is not applied.',