12ca2b89bae804121907fbc9b58f9b6d7f4290a5
[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 * 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.
64 *
65 * @param string $name
66 * @param string $password
67 * @param array $params To pass to second request
68 * @return array Result of second doApiRequest
69 */
70 private function doUserLogin( $name, $password, array $params = [] ) {
71 $ret = $this->doApiRequest( [
72 'action' => 'login',
73 'lgname' => $name,
74 ] );
75
76 $this->assertSame( 'NeedToken', $ret[0]['login']['result'] );
77
78 return $this->doApiRequest( array_merge(
79 [
80 'action' => 'login',
81 'lgtoken' => $ret[0]['login']['token'],
82 'lgname' => $name,
83 'lgpassword' => $password,
84 ], $params
85 ), $ret[2] );
86 }
87
88 public function testBadToken() {
89 $user = self::$users['sysop'];
90 $userName = $user->getUser()->getName();
91 $password = $user->getPassword();
92 $user->getUser()->logout();
93
94 $ret = $this->doUserLogin( $userName, $password, [ 'lgtoken' => 'invalid token' ] );
95
96 $this->assertSame( 'WrongToken', $ret[0]['login']['result'] );
97 }
98
99 public function testBadPass() {
100 $user = self::$users['sysop'];
101 $userName = $user->getUser()->getName();
102 $user->getUser()->logout();
103
104 $ret = $this->doUserLogin( $userName, 'bad' );
105
106 $this->assertSame( 'Failed', $ret[0]['login']['result'] );
107 }
108
109 /**
110 * @dataProvider provideEnableBotPasswords
111 */
112 public function testGoodPass( $enableBotPasswords ) {
113 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
114
115 $user = self::$users['sysop'];
116 $userName = $user->getUser()->getName();
117 $password = $user->getPassword();
118 $user->getUser()->logout();
119
120 $ret = $this->doUserLogin( $userName, $password );
121
122 $this->assertSame( 'Success', $ret[0]['login']['result'] );
123 $this->assertSame(
124 [ 'warnings' => ApiErrorFormatter::stripMarkup( wfMessage(
125 'apiwarn-deprecation-login-' . ( $enableBotPasswords ? '' : 'no' ) . 'botpw' )->
126 text() ) ],
127 $ret[0]['warnings']['login']
128 );
129 }
130
131 /**
132 * @dataProvider provideEnableBotPasswords
133 */
134 public function testUnsupportedAuthResponseType( $enableBotPasswords ) {
135 $this->setMwGlobals( 'wgEnableBotPasswords', $enableBotPasswords );
136
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' )
144 )
145 );
146 $mockProvider->method( 'getAuthenticationRequests' )
147 ->willReturn( [] );
148
149 $this->mergeMwGlobalArrayValue( 'wgAuthManagerConfig', [
150 'secondaryauth' => [ [
151 'factory' => function () use ( $mockProvider ) {
152 return $mockProvider;
153 },
154 ] ],
155 ] );
156
157 $user = self::$users['sysop'];
158 $userName = $user->getUser()->getName();
159 $password = $user->getPassword();
160 $user->getUser()->logout();
161
162 $ret = $this->doUserLogin( $userName, $password );
163
164 $this->assertSame( [ 'login' => [
165 'result' => 'Aborted',
166 'reason' => ApiErrorFormatter::stripMarkup( wfMessage(
167 'api-login-fail-aborted' . ( $enableBotPasswords ? '' : '-nobotpw' ) )->text() ),
168 ] ], $ret[0] );
169 }
170
171 /**
172 * @todo Should this test just be deleted?
173 * @group Broken
174 */
175 public function testGotCookie() {
176 $this->markTestIncomplete( "The server can't do external HTTP requests, "
177 . "and the internal one won't give cookies" );
178
179 global $wgServer, $wgScriptPath;
180
181 $user = self::$users['sysop'];
182 $userName = $user->getUser()->getName();
183 $password = $user->getPassword();
184
185 $req = MWHttpRequest::factory(
186 self::$apiUrl . '?action=login&format=json',
187 [
188 'method' => 'POST',
189 'postData' => [
190 'lgname' => $userName,
191 'lgpassword' => $password,
192 ],
193 ],
194 __METHOD__
195 );
196 $req->execute();
197
198 $content = json_decode( $req->getContent() );
199
200 $this->assertSame( 'NeedToken', $content->login->result );
201
202 $req->setData( [
203 'lgtoken' => $content->login->token,
204 'lgname' => $userName,
205 'lgpassword' => $password,
206 ] );
207 $req->execute();
208
209 $cj = $req->getCookieJar();
210 $serverName = parse_url( $wgServer, PHP_URL_HOST );
211 $this->assertNotEquals( false, $serverName );
212 $serializedCookie = $cj->serializeToHttpRequest( $wgScriptPath, $serverName );
213 $this->assertRegExp(
214 '/_session=[^;]*; .*UserID=[0-9]*; .*UserName=' . $userName . '; .*Token=/',
215 $serializedCookie
216 );
217 }
218
219 /**
220 * @return [ $username, $password ] suitable for passing to an API request for successful login
221 */
222 private function setUpForBotPassword() {
223 global $wgSessionProviders;
224
225 $this->setMwGlobals( [
226 // We can't use mergeMwGlobalArrayValue because it will overwrite the existing entry
227 // with index 0
228 'wgSessionProviders' => array_merge( $wgSessionProviders, [
229 [
230 'class' => BotPasswordSessionProvider::class,
231 'args' => [ [ 'priority' => 40 ] ],
232 ],
233 ] ),
234 'wgEnableBotPasswords' => true,
235 'wgBotPasswordsDatabase' => false,
236 'wgCentralIdLookupProvider' => 'local',
237 'wgGrantPermissions' => [
238 'test' => [ 'read' => true ],
239 ],
240 ] );
241
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();
248 }
249 $this->assertNotNull(
250 SessionManager::singleton()->getProvider( BotPasswordSessionProvider::class ),
251 'sanity check'
252 );
253
254 $user = self::$users['sysop'];
255 $centralId = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
256 $this->assertNotSame( 0, $centralId, 'sanity check' );
257
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 );
262
263 $dbw = wfGetDB( DB_MASTER );
264 $dbw->insert(
265 'bot_passwords',
266 [
267 'bp_user' => $centralId,
268 'bp_app_id' => 'foo',
269 'bp_password' => $passwordHash->toString(),
270 'bp_token' => '',
271 'bp_restrictions' => MWRestrictions::newDefault()->toJson(),
272 'bp_grants' => '["test"]',
273 ],
274 __METHOD__
275 );
276
277 $lgName = $user->getUser()->getName() . BotPassword::getSeparator() . 'foo';
278
279 return [ $lgName, $password ];
280 }
281
282 public function testBotPassword() {
283 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
284
285 $this->assertSame( 'Success', $ret[0]['login']['result'] );
286 }
287
288 public function testBotPasswordThrottled() {
289 global $wgPasswordAttemptThrottle;
290
291 $this->setGroupPermissions( 'sysop', 'noratelimit', false );
292 $this->setMwGlobals( 'wgMainCacheType', 'hash' );
293
294 list( $name, $password ) = $this->setUpForBotPassword();
295
296 for ( $i = 0; $i < $wgPasswordAttemptThrottle[0]['count']; $i++ ) {
297 $this->doUserLogin( $name, 'incorrectpasswordincorrectpassword' );
298 }
299
300 $ret = $this->doUserLogin( $name, $password );
301
302 $this->assertSame( [
303 'result' => 'Failed',
304 'reason' => ApiErrorFormatter::stripMarkup( wfMessage( 'login-throttled' )->
305 durationParams( $wgPasswordAttemptThrottle[0]['seconds'] )->text() ),
306 ], $ret[0]['login'] );
307 }
308
309 public function testBotPasswordLocked() {
310 $this->setTemporaryHook( 'UserIsLocked', function ( User $unused, &$isLocked ) {
311 $isLocked = true;
312 return true;
313 } );
314
315 $ret = $this->doUserLogin( ...$this->setUpForBotPassword() );
316
317 $this->assertSame( [
318 'result' => 'Failed',
319 'reason' => wfMessage( 'botpasswords-locked' )->text(),
320 ], $ret[0]['login'] );
321 }
322
323 public function testNoSameOriginSecurity() {
324 $this->setTemporaryHook( 'RequestHasSameOriginSecurity',
325 function () {
326 return false;
327 }
328 );
329
330 $ret = $this->doApiRequest( [
331 'action' => 'login',
332 'errorformat' => 'plaintext',
333 ] )[0]['login'];
334
335 $this->assertSame( [
336 'result' => 'Aborted',
337 'reason' => [
338 'code' => 'api-login-fail-sameorigin',
339 'text' => 'Cannot log in when the same-origin policy is not applied.',
340 ],
341 ], $ret );
342 }
343 }