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 * Attempts to log in with the given name and password, retrieves the returned token, and makes
63 * a second API request to actually log in with the token.
66 * @param string $password
67 * @param array $params To pass to second request
68 * @return array Result of second doApiRequest
70 private function doUserLogin( $name, $password, array $params = [] ) {
71 $ret = $this->doApiRequest( [
76 $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
78 return $this->doApiRequest( array_merge(
81 'lgtoken' => $ret[0]['login']['token'],
83 'lgpassword' => $password,
88 public function testBadToken() {
89 $user = self
::$users['sysop'];
90 $userName = $user->getUser()->getName();
91 $password = $user->getPassword();
92 $user->getUser()->logout();
94 $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
96 $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
99 public function testBadPass() {
100 $user = self
::$users['sysop'];
101 $userName = $user->getUser()->getName();
102 $user->getUser()->logout();
104 $ret = $this->doUserLogin( $userName, 'bad' );
106 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
110 * @dataProvider provideEnableBotPasswords
112 public function testGoodPass( $enableBotPasswords ) {
113 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
115 $user = self
::$users['sysop'];
116 $userName = $user->getUser()->getName();
117 $password = $user->getPassword();
118 $user->getUser()->logout();
120 $ret = $this->doUserLogin( $userName, $password );
122 $this->assertSame( 'Success', $ret[0]['login']['result'] );
124 [ 'warnings' => ApiErrorFormatter
::stripMarkup( wfMessage(
125 'apiwarn-deprecation-login-' . ( $enableBotPasswords ?
'' : 'no' ) . 'botpw' )->
127 $ret[0]['warnings']['login']
132 * @dataProvider provideEnableBotPasswords
134 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
135 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
137 $mockProvider = $this->createMock(
138 MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
::class );
139 $mockProvider->method( 'beginSecondaryAuthentication' )->willReturn(
140 MediaWiki\Auth\AuthenticationResponse
::newUI(
141 [ new MediaWiki\Auth\UsernameAuthenticationRequest
],
142 // Slightly silly message here
143 wfMessage( 'mainpage' )
146 $mockProvider->method( 'getAuthenticationRequests' )
149 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
150 'secondaryauth' => [ [
151 'factory' => function () use ( $mockProvider ) {
152 return $mockProvider;
157 $user = self
::$users['sysop'];
158 $userName = $user->getUser()->getName();
159 $password = $user->getPassword();
160 $user->getUser()->logout();
162 $ret = $this->doUserLogin( $userName, $password );
164 $this->assertSame( [ 'login' => [
165 'result' => 'Aborted',
166 'reason' => ApiErrorFormatter
::stripMarkup( wfMessage(
167 'api-login-fail-aborted' . ( $enableBotPasswords ?
'' : '-nobotpw' ) )->text() ),
172 * @todo Should this test just be deleted?
175 public function testGotCookie() {
176 $this->markTestIncomplete( "The server can't do external HTTP requests, "
177 . "and the internal one won't give cookies" );
179 global $wgServer, $wgScriptPath;
181 $user = self
::$users['sysop'];
182 $userName = $user->getUser()->getName();
183 $password = $user->getPassword();
185 $req = MWHttpRequest
::factory(
186 self
::$apiUrl . '?action=login&format=json',
190 'lgname' => $userName,
191 'lgpassword' => $password,
198 $content = json_decode( $req->getContent() );
200 $this->assertSame( 'NeedToken', $content->login
->result
);
203 'lgtoken' => $content->login
->token
,
204 'lgname' => $userName,
205 'lgpassword' => $password,
209 $cj = $req->getCookieJar();
210 $serverName = parse_url( $wgServer, PHP_URL_HOST
);
211 $this->assertNotEquals( false, $serverName );
212 $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
214 '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
220 * @return [ $username, $password ] suitable for passing to an API request for successful login
222 private function setUpForBotPassword() {
223 global $wgSessionProviders;
225 $this->setMwGlobals( [
226 // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
228 'wgSessionProviders' => array_merge( $wgSessionProviders, [
230 'class' => BotPasswordSessionProvider
::class,
231 'args' => [ [ 'priority' => 40 ] ],
234 'wgEnableBotPasswords' => true,
235 'wgBotPasswordsDatabase' => false,
236 'wgCentralIdLookupProvider' => 'local',
237 'wgGrantPermissions' => [
238 'test' => [ 'read' => true ],
242 // Make sure our session provider is present
243 $manager = TestingAccessWrapper
::newFromObject( SessionManager
::singleton() );
244 if ( !isset( $manager->sessionProviders
[BotPasswordSessionProvider
::class] ) ) {
245 $tmp = $manager->sessionProviders
;
246 $manager->sessionProviders
= null;
247 $manager->sessionProviders
= $tmp +
$manager->getProviders();
249 $this->assertNotNull(
250 SessionManager
::singleton()->getProvider( BotPasswordSessionProvider
::class ),
254 $user = self
::$users['sysop'];
255 $centralId = CentralIdLookup
::factory()->centralIdFromLocalUser( $user->getUser() );
256 $this->assertNotSame( 0, $centralId, 'sanity check' );
258 $password = 'ngfhmjm64hv0854493hsj5nncjud2clk';
259 $passwordFactory = MediaWikiServices
::getInstance()->getPasswordFactory();
260 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
261 $passwordHash = $passwordFactory->newFromPlaintext( $password );
263 $dbw = wfGetDB( DB_MASTER
);
267 'bp_user' => $centralId,
268 'bp_app_id' => 'foo',
269 'bp_password' => $passwordHash->toString(),
271 'bp_restrictions' => MWRestrictions
::newDefault()->toJson(),
272 'bp_grants' => '["test"]',
277 $lgName = $user->getUser()->getName() . BotPassword
::getSeparator() . 'foo';
279 return [ $lgName, $password ];
282 public function testBotPassword() {
283 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
285 $this->assertSame( 'Success', $ret[0]['login']['result'] );
288 public function testBotPasswordThrottled() {
289 global $wgPasswordAttemptThrottle;
291 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
292 $this->setMwGlobals( 'wgMainCacheType', 'hash' );
294 list( $name, $password ) = $this->setUpForBotPassword();
296 for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++
) {
297 $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
300 $ret = $this->doUserLogin( $name, $password );
303 'result' => 'Failed',
304 'reason' => ApiErrorFormatter
::stripMarkup( wfMessage( 'login-throttled' )->
305 durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
306 ], $ret[0]['login'] );
309 public function testBotPasswordLocked() {
310 $this->setTemporaryHook( 'UserIsLocked', function ( User
$unused, &$isLocked ) {
315 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
318 'result' => 'Failed',
319 'reason' => wfMessage( 'botpasswords-locked' )->text(),
320 ], $ret[0]['login'] );
323 public function testNoSameOriginSecurity() {
324 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
330 $ret = $this->doApiRequest( [
332 'errorformat' => 'plaintext',
336 'result' => 'Aborted',
338 'code' => 'api-login-fail-sameorigin',
339 'text' => 'Cannot log in when the same-origin policy is not applied.',