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
;
16 use Wikimedia\TestingAccessWrapper
;
23 * @covers \MediaWiki\Api\ApiLogin
25 class ApiLoginTest
extends ApiTestCase
{
27 public static function provideEnableBotPasswords() {
29 'Bot passwords enabled' => [ true ],
30 'Bot passwords disabled' => [ false ],
35 * @dataProvider provideEnableBotPasswords
37 public function testExtendedDescription( $enableBotPasswords ) {
38 $this->overrideConfigValue(
39 MainConfigNames
::EnableBotPasswords
,
42 $ret = $this->doApiRequest( [
43 'action' => 'paraminfo',
45 'helpformat' => 'raw',
48 'apihelp-login-extended-description' . ( $enableBotPasswords ?
'' : '-nobotpasswords' ),
49 $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
54 * Test result of attempted login with an empty username
56 public function testNoName() {
58 'wsTokenSecrets' => [ 'login' => 'foobar' ],
60 $ret = $this->doApiRequest( [
63 'lgpassword' => $this->getTestSysop()->getPassword(),
64 'lgtoken' => (string)( new Token( 'foobar', '' ) ),
66 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
70 * @dataProvider provideEnableBotPasswords
72 public function testDeprecatedUserLogin( $enableBotPasswords ) {
73 $this->overrideConfigValue(
74 MainConfigNames
::EnableBotPasswords
,
78 $user = $this->getTestUser();
80 $ret = $this->doApiRequest( [
82 'lgname' => $user->getUser()->getName(),
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( [
94 'lgtoken' => $ret[0]['login']['token'],
95 'lgname' => $user->getUser()->getName(),
96 'lgpassword' => $user->getPassword(),
100 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
101 'apiwarn-deprecation-login-' . ( $enableBotPasswords ?
'' : 'no' ) . 'botpw' )
103 $ret[0]['warnings']['login']
107 'result' => 'Success',
108 'lguserid' => $user->getUser()->getId(),
109 'lgusername' => $user->getUser()->getName(),
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( [
131 $this->assertArrayNotHasKey( 'warnings', $ret );
133 return $this->doApiRequest( array_merge(
136 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
138 'lgpassword' => $password,
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( [
166 $this->assertArrayNotHasKey( 'warnings', $ret );
169 SessionManager
::getGlobalSession()->clear();
172 $ret = $this->doApiRequest( [
174 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
175 'lgname' => $userName,
176 'lgpassword' => $password,
177 'errorformat' => 'raw',
181 'result' => 'Failed',
183 'code' => 'sessionlost',
184 'key' => 'authpage-cannot-login-continue',
187 ], $ret[0]['login'] );
190 public function testBadPass() {
191 $user = $this->getTestSysop()->getUser();
192 $userName = $user->getName();
195 $ret = $this->doUserLogin( $userName, 'bad', [ 'errorformat' => 'raw' ] );
198 'result' => 'Failed',
200 'code' => 'wrongpassword',
201 'key' => 'wrongpassword',
204 ], $ret[0]['login'] );
208 * @dataProvider provideEnableBotPasswords
210 public function testGoodPass( $enableBotPasswords ) {
211 $this->overrideConfigValue(
212 MainConfigNames
::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'] );
225 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
226 'apiwarn-deprecation-login-' . ( $enableBotPasswords ?
'' : 'no' ) . 'botpw' )->
228 $ret[0]['warnings']['login']
233 * @dataProvider provideEnableBotPasswords
235 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
236 $this->overrideConfigValue(
237 MainConfigNames
::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' )
253 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
254 'secondaryauth' => [ [
255 'factory' => static function () use ( $mockProvider ) {
256 return $mockProvider;
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() ),
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
284 MainConfigNames
::SessionProviders
=> array_merge( $wgSessionProviders, [
286 'class' => BotPasswordSessionProvider
::class,
287 'args' => [ [ 'priority' => 40 ] ],
288 'services' => [ 'GrantsInfo' ],
291 MainConfigNames
::EnableBotPasswords
=> true,
292 MainConfigNames
::CentralIdLookupProvider
=> 'local',
293 MainConfigNames
::GrantPermissions
=> [
294 'test' => [ 'read' => true ],
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' )
323 'bp_user' => $centralId,
324 'bp_app_id' => 'foo',
325 'bp_password' => $passwordHash->toString(),
327 'bp_restrictions' => MWRestrictions
::newDefault()->toJson(),
328 'bp_grants' => '["test"]',
330 ->caller( __METHOD__
)
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
347 [ 'count' => 5, 'seconds' => 30 ],
348 [ 'count' => 100, 'seconds' => 60 * 60 * 48 ],
351 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
352 $this->overrideConfigValue(
353 MainConfigNames
::PasswordAttemptThrottle
,
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 );
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 ) {
378 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
381 'result' => 'Failed',
382 'reason' => wfMessage( 'botpasswords-locked' )->text(),
383 ], $ret[0]['login'] );
386 public function testNoSameOriginSecurity() {
387 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
393 $ret = $this->doApiRequest( [
395 'errorformat' => 'plaintext',
399 'result' => 'Aborted',
401 'code' => 'api-login-fail-sameorigin',
402 'text' => 'Cannot log in when the same-origin policy is not applied.',