Remove hard deprecation of PasswordPolicyChecks::checkPopularPasswordBlacklist
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiLoginTest.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use MediaWiki\Session\BotPasswordSessionProvider;
5 use MediaWiki\Session\SessionManager;
6 use Wikimedia\TestingAccessWrapper;
7
8 /**
9 * @group API
10 * @group Database
11 * @group medium
12 *
13 * @covers ApiLogin
14 */
15 class ApiLoginTest extends ApiTestCase {
16 public function setUp() {
17 parent::setUp();
18
19 $this->tablesUsed[] = 'bot_passwords';
20 }
21
22 public static function provideEnableBotPasswords() {
23 return [
24 'Bot passwords enabled' => [ true ],
25 'Bot passwords disabled' => [ false ],
26 ];
27 }
28
29 /**
30 * @dataProvider provideEnableBotPasswords
31 */
32 public function testExtendedDescription( $enableBotPasswords ) {
33 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
34 $ret = $this->doApiRequest( [
35 'action' => 'paraminfo',
36 'modules' => 'login',
37 'helpformat' => 'raw',
38 ] );
39 $this->assertSame(
40 'apihelp-login-extended-description' . ( $enableBotPasswords ? '' : '-nobotpasswords' ),
41 $ret[0]['paraminfo']['modules'][0]['description'][1]['key']
42 );
43 }
44
45 /**
46 * Test result of attempted login with an empty username
47 */
48 public function testNoName() {
49 $session = [
50 'wsTokenSecrets' => [ 'login' => 'foobar' ],
51 ];
52 $ret = $this->doApiRequest( [
53 'action' => 'login',
54 'lgname' => '',
55 'lgpassword' => self::$users['sysop']->getPassword(),
56 'lgtoken' => (string)( new MediaWiki\Session\Token( 'foobar', '' ) ),
57 ], $session );
58 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
59 }
60
61 /**
62 * @dataProvider provideEnableBotPasswords
63 */
64 public function testDeprecatedUserLogin( $enableBotPasswords ) {
65 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
66
67 $user = $this->getTestUser();
68
69 $ret = $this->doApiRequest( [
70 'action' => 'login',
71 'lgname' => $user->getUser()->getName(),
72 ] );
73
74 $this->assertSame(
75 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
76 'apiwarn-deprecation-login-token' )->text() ) ],
77 $ret[0]['warnings']['login']
78 );
79 $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
80
81 $ret = $this->doApiRequest( [
82 'action' => 'login',
83 'lgtoken' => $ret[0]['login']['token'],
84 'lgname' => $user->getUser()->getName(),
85 'lgpassword' => $user->getPassword(),
86 ], $ret[2] );
87
88 $this->assertSame(
89 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
90 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )
91 ->text() ) ],
92 $ret[0]['warnings']['login']
93 );
94 $this->assertSame(
95 [
96 'result' => 'Success',
97 'lguserid' => $user->getUser()->getId(),
98 'lgusername' => $user->getUser()->getName(),
99 ],
100 $ret[0]['login']
101 );
102 }
103
104 /**
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.
107 *
108 * @param string $name
109 * @param string $password
110 * @param array $params To pass to second request
111 * @return array Result of second doApiRequest
112 */
113 private function doUserLogin( $name, $password, array $params = [] ) {
114 $ret = $this->doApiRequest( [
115 'action' => 'query',
116 'meta' => 'tokens',
117 'type' => 'login',
118 ] );
119
120 $this->assertArrayNotHasKey( 'warnings', $ret );
121
122 return $this->doApiRequest( array_merge(
123 [
124 'action' => 'login',
125 'lgtoken' => $ret[0]['query']['tokens']['logintoken'],
126 'lgname' => $name,
127 'lgpassword' => $password,
128 ], $params
129 ), $ret[2] );
130 }
131
132 public function testBadToken() {
133 $user = self::$users['sysop'];
134 $userName = $user->getUser()->getName();
135 $password = $user->getPassword();
136 $user->getUser()->logout();
137
138 $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
139
140 $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
141 }
142
143 public function testBadPass() {
144 $user = self::$users['sysop'];
145 $userName = $user->getUser()->getName();
146 $user->getUser()->logout();
147
148 $ret = $this->doUserLogin( $userName, 'bad' );
149
150 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
151 }
152
153 /**
154 * @dataProvider provideEnableBotPasswords
155 */
156 public function testGoodPass( $enableBotPasswords ) {
157 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
158
159 $user = self::$users['sysop'];
160 $userName = $user->getUser()->getName();
161 $password = $user->getPassword();
162 $user->getUser()->logout();
163
164 $ret = $this->doUserLogin( $userName, $password );
165
166 $this->assertSame( 'Success', $ret[0]['login']['result'] );
167 $this->assertSame(
168 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
169 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
170 text() ) ],
171 $ret[0]['warnings']['login']
172 );
173 }
174
175 /**
176 * @dataProvider provideEnableBotPasswords
177 */
178 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
179 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
180
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' )
188 )
189 );
190 $mockProvider->method( 'getAuthenticationRequests' )
191 ->willReturn( [] );
192
193 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
194 'secondaryauth' => [ [
195 'factory' => function () use ( $mockProvider ) {
196 return $mockProvider;
197 },
198 ] ],
199 ] );
200
201 $user = self::$users['sysop'];
202 $userName = $user->getUser()->getName();
203 $password = $user->getPassword();
204 $user->getUser()->logout();
205
206 $ret = $this->doUserLogin( $userName, $password );
207
208 $this->assertSame( [ 'login' => [
209 'result' => 'Aborted',
210 'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
211 'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
212 ] ], $ret[0] );
213 }
214
215 /**
216 * @todo Should this test just be deleted?
217 * @group Broken
218 */
219 public function testGotCookie() {
220 $this->markTestIncomplete( "The server can't do external HTTP requests, "
221 . "and the internal one won't give cookies" );
222
223 global $wgServer, $wgScriptPath;
224
225 $user = self::$users['sysop'];
226 $userName = $user->getUser()->getName();
227 $password = $user->getPassword();
228
229 $req = MWHttpRequest::factory(
230 self::$apiUrl . '?action=login&format=json',
231 [
232 'method' => 'POST',
233 'postData' => [
234 'lgname' => $userName,
235 'lgpassword' => $password,
236 ],
237 ],
238 __METHOD__
239 );
240 $req->execute();
241
242 $content = json_decode( $req->getContent() );
243
244 $this->assertSame( 'NeedToken', $content->login->result );
245
246 $req->setData( [
247 'lgtoken' => $content->login->token,
248 'lgname' => $userName,
249 'lgpassword' => $password,
250 ] );
251 $req->execute();
252
253 $cj = $req->getCookieJar();
254 $serverName = parse_url( $wgServer, PHP_URL_HOST );
255 $this->assertNotEquals( false, $serverName );
256 $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
257 $this->assertRegExp(
258 '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
259 $serializedCookie
260 );
261 }
262
263 /**
264 * @return [ $username, $password ] suitable for passing to an API request for successful login
265 */
266 private function setUpForBotPassword() {
267 global $wgSessionProviders;
268
269 $this->setMwGlobals( [
270 // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
271 // with index 0
272 'wgSessionProviders' => array_merge( $wgSessionProviders, [
273 [
274 'class' => BotPasswordSessionProvider::class,
275 'args' => [ [ 'priority' => 40 ] ],
276 ],
277 ] ),
278 'wgEnableBotPasswords' => true,
279 'wgBotPasswordsDatabase' => false,
280 'wgCentralIdLookupProvider' => 'local',
281 'wgGrantPermissions' => [
282 'test' => [ 'read' => true ],
283 ],
284 ] );
285
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();
292 }
293 $this->assertNotNull(
294 SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class ),
295 'sanity check'
296 );
297
298 $user = self::$users['sysop'];
299 $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
300 $this->assertNotSame( 0, $centralId, 'sanity check' );
301
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 );
306
307 $dbw = wfGetDB( DB_MASTER );
308 $dbw->insert(
309 'bot_passwords',
310 [
311 'bp_user' => $centralId,
312 'bp_app_id' => 'foo',
313 'bp_password' => $passwordHash->toString(),
314 'bp_token' => '',
315 'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
316 'bp_grants' => '["test"]',
317 ],
318 __METHOD__
319 );
320
321 $lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
322
323 return [ $lgName, $password ];
324 }
325
326 public function testBotPassword() {
327 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
328
329 $this->assertSame( 'Success', $ret[0]['login']['result'] );
330 }
331
332 public function testBotPasswordThrottled() {
333 global $wgPasswordAttemptThrottle;
334
335 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
336 $this->setMwGlobals( 'wgMainCacheType', 'hash' );
337
338 list( $name, $password ) = $this->setUpForBotPassword();
339
340 for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++ ) {
341 $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
342 }
343
344 $ret = $this->doUserLogin( $name, $password );
345
346 $this->assertSame( [
347 'result' => 'Failed',
348 'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
349 durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
350 ], $ret[0]['login'] );
351 }
352
353 public function testBotPasswordLocked() {
354 $this->setTemporaryHook( 'UserIsLocked', function ( User $unused, &$isLocked ) {
355 $isLocked = true;
356 return true;
357 } );
358
359 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
360
361 $this->assertSame( [
362 'result' => 'Failed',
363 'reason' => wfMessage( 'botpasswords-locked' )->text(),
364 ], $ret[0]['login'] );
365 }
366
367 public function testNoSameOriginSecurity() {
368 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
369 function () {
370 return false;
371 }
372 );
373
374 $ret = $this->doApiRequest( [
375 'action' => 'login',
376 'errorformat' => 'plaintext',
377 ] )[0]['login'];
378
379 $this->assertSame( [
380 'result' => 'Aborted',
381 'reason' => [
382 'code' => 'api-login-fail-sameorigin',
383 'text' => 'Cannot log in when the same-origin policy is not applied.',
384 ],
385 ], $ret );
386 }
387 }