Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / api / ApiLoginTest.php
blobd1ac44d30ed674e7980b05df683aa62d9508bea7
1 <?php
3 namespace MediaWiki\Tests\Api;
5 use MediaWiki\Api\ApiErrorFormatter;
6 use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider;
7 use MediaWiki\Auth\AuthenticationResponse;
8 use MediaWiki\Auth\UsernameAuthenticationRequest;
9 use MediaWiki\MainConfigNames;
10 use MediaWiki\Session\BotPasswordSessionProvider;
11 use MediaWiki\Session\SessionManager;
12 use MediaWiki\Session\Token;
13 use MediaWiki\User\BotPassword;
14 use MediaWiki\User\User;
15 use MWRestrictions;
16 use Wikimedia\TestingAccessWrapper;
18 /**
19 * @group API
20 * @group Database
21 * @group medium
23 * @covers \MediaWiki\Api\ApiLogin
25 class ApiLoginTest extends ApiTestCase {
27 public static function provideEnableBotPasswords() {
28 return [
29 'Bot passwords enabled' => [ true ],
30 'Bot passwords disabled' => [ false ],
34 /**
35 * @dataProvider provideEnableBotPasswords
37 public function testExtendedDescription( $enableBotPasswords ) {
38 $this->overrideConfigValue(
39 MainConfigNames::EnableBotPasswords,
40 $enableBotPasswords
42 $ret = $this->doApiRequest( [
43 'action' => 'paraminfo',
44 'modules' => 'login',
45 'helpformat' => 'raw',
46 ] );
47 $this->assertSame(
48 'apihelp-login-extended-description' . ( $enableBotPasswords ? '' : '-nobotpasswords' ),
49 $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
53 /**
54 * Test result of attempted login with an empty username
56 public function testNoName() {
57 $session = [
58 'wsTokenSecrets' => [ 'login' => 'foobar' ],
60 $ret = $this->doApiRequest( [
61 'action' => 'login',
62 'lgname' => '',
63 'lgpassword' => $this->getTestSysop()->getPassword(),
64 'lgtoken' => (string)( new Token( 'foobar', '' ) ),
65 ], $session );
66 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
69 /**
70 * @dataProvider provideEnableBotPasswords
72 public function testDeprecatedUserLogin( $enableBotPasswords ) {
73 $this->overrideConfigValue(
74 MainConfigNames::EnableBotPasswords,
75 $enableBotPasswords
78 $user = $this->getTestUser();
80 $ret = $this->doApiRequest( [
81 'action' => 'login',
82 'lgname' => $user->getUser()->getName(),
83 ] );
85 $this->assertSame(
86 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
87 'apiwarn-deprecation-login-token' )->text() ) ],
88 $ret[0]['warnings']['login']
90 $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
92 $ret = $this->doApiRequest( [
93 'action' => 'login',
94 'lgtoken' => $ret[0]['login']['token'],
95 'lgname' => $user->getUser()->getName(),
96 'lgpassword' => $user->getPassword(),
97 ], $ret[2] );
99 $this->assertSame(
100 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
101 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
102 ->text() ) ],
103 $ret[0]['warnings']['login']
105 $this->assertSame(
107 'result' => 'Success',
108 'lguserid' => $user->getUser()->getId(),
109 'lgusername' => $user->getUser()->getName(),
111 $ret[0]['login']
116 * Attempts to log in with the given name and password, retrieves the returned token, and makes
117 * a second API request to actually log in with the token.
119 * @param string $name
120 * @param string $password
121 * @param array $params To pass to second request
122 * @return array Result of second doApiRequest
124 private function doUserLogin( $name, $password, array $params = [] ) {
125 $ret = $this->doApiRequest( [
126 'action' => 'query',
127 'meta' => 'tokens',
128 'type' => 'login',
129 ] );
131 $this->assertArrayNotHasKey( 'warnings', $ret );
133 return $this->doApiRequest( array_merge(
135 'action' => 'login',
136 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
137 'lgname' => $name,
138 'lgpassword' => $password,
139 ], $params
140 ), $ret[2] );
143 public function testBadToken() {
144 $testUser = $this->getTestSysop();
145 $userName = $testUser->getUser()->getName();
146 $password = $testUser->getPassword();
147 $testUser->getUser()->logout();
149 $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
151 $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
154 public function testLostSession() {
155 $testUser = $this->getTestSysop();
156 $userName = $testUser->getUser()->getName();
157 $password = $testUser->getPassword();
158 $testUser->getUser()->logout();
160 $ret = $this->doApiRequest( [
161 'action' => 'query',
162 'meta' => 'tokens',
163 'type' => 'login',
164 ] );
166 $this->assertArrayNotHasKey( 'warnings', $ret );
168 // Lose the session
169 SessionManager::getGlobalSession()->clear();
170 $ret[2] = [];
172 $ret = $this->doApiRequest( [
173 'action' => 'login',
174 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
175 'lgname' => $userName,
176 'lgpassword' => $password,
177 'errorformat' => 'raw',
178 ], $ret[2] );
180 $this->assertSame( [
181 'result' => 'Failed',
182 'reason' => [
183 'code' => 'sessionlost',
184 'key' => 'authpage-cannot-login-continue',
185 'params' => [],
187 ], $ret[0]['login'] );
190 public function testBadPass() {
191 $user = $this->getTestSysop()->getUser();
192 $userName = $user->getName();
193 $user->logout();
195 $ret = $this->doUserLogin( $userName, 'bad', [ 'errorformat' => 'raw' ] );
197 $this->assertSame( [
198 'result' => 'Failed',
199 'reason' => [
200 'code' => 'wrongpassword',
201 'key' => 'wrongpassword',
202 'params' => [],
204 ], $ret[0]['login'] );
208 * @dataProvider provideEnableBotPasswords
210 public function testGoodPass( $enableBotPasswords ) {
211 $this->overrideConfigValue(
212 MainConfigNames::EnableBotPasswords,
213 $enableBotPasswords
216 $testUser = $this->getTestSysop();
217 $userName = $testUser->getUser()->getName();
218 $password = $testUser->getPassword();
219 $testUser->getUser()->logout();
221 $ret = $this->doUserLogin( $userName, $password );
223 $this->assertSame( 'Success', $ret[0]['login']['result'] );
224 $this->assertSame(
225 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
226 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
227 text() ) ],
228 $ret[0]['warnings']['login']
233 * @dataProvider provideEnableBotPasswords
235 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
236 $this->overrideConfigValue(
237 MainConfigNames::EnableBotPasswords,
238 $enableBotPasswords
241 $mockProvider = $this->createMock(
242 AbstractSecondaryAuthenticationProvider::class );
243 $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
244 AuthenticationResponse::newUI(
245 [ new UsernameAuthenticationRequest ],
246 // Slightly silly message here
247 wfMessage( 'mainpage' )
250 $mockProvider->method( 'getAuthenticationRequests' )
251 ->willReturn( [] );
253 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
254 'secondaryauth' => [ [
255 'factory' => static function () use ( $mockProvider ) {
256 return $mockProvider;
258 ] ],
259 ] );
261 $testUser = $this->getTestSysop();
262 $userName = $testUser->getUser()->getName();
263 $password = $testUser->getPassword();
264 $testUser->getUser()->logout();
266 $ret = $this->doUserLogin( $userName, $password );
268 $this->assertSame( [ 'login' => [
269 'result' => 'Aborted',
270 'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
271 'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
272 ] ], $ret[0] );
276 * @return array [ $username, $password ] suitable for passing to an API request for successful login
278 private function setUpForBotPassword() {
279 global $wgSessionProviders;
281 $this->overrideConfigValues( [
282 // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
283 // with index 0
284 MainConfigNames::SessionProviders => array_merge( $wgSessionProviders, [
286 'class' => BotPasswordSessionProvider::class,
287 'args' => [ [ 'priority' => 40 ] ],
288 'services' => [ 'GrantsInfo' ],
290 ] ),
291 MainConfigNames::EnableBotPasswords => true,
292 MainConfigNames::CentralIdLookupProvider => 'local',
293 MainConfigNames::GrantPermissions => [
294 'test' => [ 'read' => true ],
296 ] );
298 // Make sure our session provider is present
299 $manager = TestingAccessWrapper::newFromObject( SessionManager::singleton() );
300 if ( !isset( $manager->sessionProviders[BotPasswordSessionProvider::class] ) ) {
301 $tmp = $manager->sessionProviders;
302 $manager->sessionProviders = null;
303 $manager->sessionProviders = $tmp + $manager->getProviders();
305 $this->assertNotNull(
306 SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class )
309 $user = $this->getTestSysop()->getUser();
310 $centralId = $this->getServiceContainer()
311 ->getCentralIdLookup()
312 ->centralIdFromLocalUser( $user );
313 $this->assertNotSame( 0, $centralId );
315 $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
316 $passwordFactory = $this->getServiceContainer()->getPasswordFactory();
317 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
318 $passwordHash = $passwordFactory->newFromPlaintext( $password );
320 $this->getDb()->newInsertQueryBuilder()
321 ->insertInto( 'bot_passwords' )
322 ->row( [
323 'bp_user' => $centralId,
324 'bp_app_id' => 'foo',
325 'bp_password' => $passwordHash->toString(),
326 'bp_token' => '',
327 'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
328 'bp_grants' => '["test"]',
330 ->caller( __METHOD__ )
331 ->execute();
333 $lgName = $user->getName() . BotPassword::getSeparator() . 'foo';
335 return [ $lgName, $password ];
338 public function testBotPassword() {
339 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
341 $this->assertSame( 'Success', $ret[0]['login']['result'] );
344 public function testBotPasswordThrottled() {
345 // Undo high count from DevelopmentSettings.php
346 $throttle = [
347 [ 'count' => 5, 'seconds' => 30 ],
348 [ 'count' => 100, 'seconds' => 60 * 60 * 48 ],
351 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
352 $this->overrideConfigValue(
353 MainConfigNames::PasswordAttemptThrottle,
354 $throttle
357 [ $name, $password ] = $this->setUpForBotPassword();
359 for ( $i = 0; $i < $throttle[0]['count']; $i++ ) {
360 $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
363 $ret = $this->doUserLogin( $name, $password );
365 $this->assertSame( [
366 'result' => 'Failed',
367 'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
368 durationParams( $throttle[0]['seconds'] )->text() ),
369 ], $ret[0]['login'] );
372 public function testBotPasswordLocked() {
373 $this->setTemporaryHook( 'UserIsLocked', static function ( User $unused, &$isLocked ) {
374 $isLocked = true;
375 return true;
376 } );
378 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
380 $this->assertSame( [
381 'result' => 'Failed',
382 'reason' => wfMessage( 'botpasswords-locked' )->text(),
383 ], $ret[0]['login'] );
386 public function testNoSameOriginSecurity() {
387 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
388 static function () {
389 return false;
393 $ret = $this->doApiRequest( [
394 'action' => 'login',
395 'errorformat' => 'plaintext',
396 ] )[0]['login'];
398 $this->assertSame( [
399 'result' => 'Aborted',
400 'reason' => [
401 'code' => 'api-login-fail-sameorigin',
402 'text' => 'Cannot log in when the same-origin policy is not applied.',
404 ], $ret );