Merge "rdbms: Use correct value for 'sslmode' in DatabasePostgres"
[lhc/web/wiklou.git] / tests / phpunit / includes / auth / AuthManagerTest.php
1 <?php
2
3 namespace MediaWiki\Auth;
4
5 use Config;
6 use MediaWiki\Session\SessionInfo;
7 use MediaWiki\Session\UserInfo;
8 use Psr\Log\LoggerInterface;
9 use Psr\Log\LogLevel;
10 use StatusValue;
11 use WebRequest;
12 use Wikimedia\ScopedCallback;
13 use Wikimedia\TestingAccessWrapper;
14
15 /**
16 * @group AuthManager
17 * @group Database
18 * @covers \MediaWiki\Auth\AuthManager
19 */
20 class AuthManagerTest extends \MediaWikiTestCase {
21 /** @var WebRequest */
22 protected $request;
23 /** @var Config */
24 protected $config;
25 /** @var LoggerInterface */
26 protected $logger;
27
28 protected $preauthMocks = [];
29 protected $primaryauthMocks = [];
30 protected $secondaryauthMocks = [];
31
32 /** @var AuthManager */
33 protected $manager;
34 /** @var TestingAccessWrapper */
35 protected $managerPriv;
36
37 /**
38 * Sets a mock on a hook
39 * @param string $hook
40 * @param object $expect From $this->once(), $this->never(), etc.
41 * @return object $mock->expects( $expect )->method( ... ).
42 */
43 protected function hook( $hook, $expect ) {
44 $mock = $this->getMockBuilder( __CLASS__ )
45 ->setMethods( [ "on$hook" ] )
46 ->getMock();
47 $this->setTemporaryHook( $hook, $mock );
48 return $mock->expects( $expect )->method( "on$hook" );
49 }
50
51 /**
52 * Unsets a hook
53 * @param string $hook
54 */
55 protected function unhook( $hook ) {
56 global $wgHooks;
57 $wgHooks[$hook] = [];
58 }
59
60 /**
61 * Ensure a value is a clean Message object
62 * @param string|Message $key
63 * @param array $params
64 * @return Message
65 */
66 protected function message( $key, $params = [] ) {
67 if ( $key === null ) {
68 return null;
69 }
70 if ( $key instanceof \MessageSpecifier ) {
71 $params = $key->getParams();
72 $key = $key->getKey();
73 }
74 return new \Message( $key, $params, \Language::factory( 'en' ) );
75 }
76
77 /**
78 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
79 * because that recursively compares members, which leads to false negatives if e.g. Language
80 * caches are reset.
81 *
82 * @param AuthenticationResponse $response1
83 * @param AuthenticationResponse $response2
84 * @param string $msg
85 * @return bool
86 */
87 private function assertResponseEquals(
88 AuthenticationResponse $expected, AuthenticationResponse $actual, $msg = ''
89 ) {
90 foreach ( ( new \ReflectionClass( $expected ) )->getProperties() as $prop ) {
91 $name = $prop->getName();
92 $usedMsg = ltrim( "$msg ($name)" );
93 if ( $name === 'message' && $expected->message ) {
94 $this->assertSame( $expected->message->serialize(), $actual->message->serialize(),
95 $usedMsg );
96 } else {
97 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
98 }
99 }
100 }
101
102 /**
103 * Initialize the AuthManagerConfig variable in $this->config
104 *
105 * Uses data from the various 'mocks' fields.
106 */
107 protected function initializeConfig() {
108 $config = [
109 'preauth' => [
110 ],
111 'primaryauth' => [
112 ],
113 'secondaryauth' => [
114 ],
115 ];
116
117 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
118 $key = $type . 'Mocks';
119 foreach ( $this->$key as $mock ) {
120 $config[$type][$mock->getUniqueId()] = [ 'factory' => function () use ( $mock ) {
121 return $mock;
122 } ];
123 }
124 }
125
126 $this->config->set( 'AuthManagerConfig', $config );
127 $this->config->set( 'LanguageCode', 'en' );
128 $this->config->set( 'NewUserLog', false );
129 }
130
131 /**
132 * Initialize $this->manager
133 * @param bool $regen Force a call to $this->initializeConfig()
134 */
135 protected function initializeManager( $regen = false ) {
136 if ( $regen || !$this->config ) {
137 $this->config = new \HashConfig();
138 }
139 if ( $regen || !$this->request ) {
140 $this->request = new \FauxRequest();
141 }
142 if ( !$this->logger ) {
143 $this->logger = new \TestLogger();
144 }
145
146 if ( $regen || !$this->config->has( 'AuthManagerConfig' ) ) {
147 $this->initializeConfig();
148 }
149 $this->manager = new AuthManager( $this->request, $this->config );
150 $this->manager->setLogger( $this->logger );
151 $this->managerPriv = TestingAccessWrapper::newFromObject( $this->manager );
152 }
153
154 /**
155 * Setup SessionManager with a mock session provider
156 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
157 * @param array $methods Additional methods to mock
158 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
159 */
160 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
161 if ( !$this->config ) {
162 $this->config = new \HashConfig();
163 $this->initializeConfig();
164 }
165 $this->config->set( 'ObjectCacheSessionExpiry', 100 );
166
167 $methods[] = '__toString';
168 $methods[] = 'describe';
169 if ( $canChangeUser !== null ) {
170 $methods[] = 'canChangeUser';
171 }
172 $provider = $this->getMockBuilder( \DummySessionProvider::class )
173 ->setMethods( $methods )
174 ->getMock();
175 $provider->expects( $this->any() )->method( '__toString' )
176 ->will( $this->returnValue( 'MockSessionProvider' ) );
177 $provider->expects( $this->any() )->method( 'describe' )
178 ->will( $this->returnValue( 'MockSessionProvider sessions' ) );
179 if ( $canChangeUser !== null ) {
180 $provider->expects( $this->any() )->method( 'canChangeUser' )
181 ->will( $this->returnValue( $canChangeUser ) );
182 }
183 $this->config->set( 'SessionProviders', [
184 [ 'factory' => function () use ( $provider ) {
185 return $provider;
186 } ],
187 ] );
188
189 $manager = new \MediaWiki\Session\SessionManager( [
190 'config' => $this->config,
191 'logger' => new \Psr\Log\NullLogger(),
192 'store' => new \HashBagOStuff(),
193 ] );
194 TestingAccessWrapper::newFromObject( $manager )->getProvider( (string)$provider );
195
196 $reset = \MediaWiki\Session\TestUtils::setSessionManagerSingleton( $manager );
197
198 if ( $this->request ) {
199 $manager->getSessionForRequest( $this->request );
200 }
201
202 return [ $provider, $reset ];
203 }
204
205 public function testSingleton() {
206 // Temporarily clear out the global singleton, if any, to test creating
207 // one.
208 $rProp = new \ReflectionProperty( AuthManager::class, 'instance' );
209 $rProp->setAccessible( true );
210 $old = $rProp->getValue();
211 $cb = new ScopedCallback( [ $rProp, 'setValue' ], [ $old ] );
212 $rProp->setValue( null );
213
214 $singleton = AuthManager::singleton();
215 $this->assertInstanceOf( AuthManager::class, AuthManager::singleton() );
216 $this->assertSame( $singleton, AuthManager::singleton() );
217 $this->assertSame( \RequestContext::getMain()->getRequest(), $singleton->getRequest() );
218 $this->assertSame(
219 \RequestContext::getMain()->getConfig(),
220 TestingAccessWrapper::newFromObject( $singleton )->config
221 );
222 }
223
224 public function testCanAuthenticateNow() {
225 $this->initializeManager();
226
227 list( $provider, $reset ) = $this->getMockSessionProvider( false );
228 $this->assertFalse( $this->manager->canAuthenticateNow() );
229 ScopedCallback::consume( $reset );
230
231 list( $provider, $reset ) = $this->getMockSessionProvider( true );
232 $this->assertTrue( $this->manager->canAuthenticateNow() );
233 ScopedCallback::consume( $reset );
234 }
235
236 public function testNormalizeUsername() {
237 $mocks = [
238 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
239 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
240 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
241 $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
242 ];
243 foreach ( $mocks as $key => $mock ) {
244 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
245 }
246 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
247 ->with( $this->identicalTo( 'XYZ' ) )
248 ->willReturn( 'Foo' );
249 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
250 ->with( $this->identicalTo( 'XYZ' ) )
251 ->willReturn( 'Foo' );
252 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
253 ->with( $this->identicalTo( 'XYZ' ) )
254 ->willReturn( null );
255 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
256 ->with( $this->identicalTo( 'XYZ' ) )
257 ->willReturn( 'Bar!' );
258
259 $this->primaryauthMocks = $mocks;
260
261 $this->initializeManager();
262
263 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager->normalizeUsername( 'XYZ' ) );
264 }
265
266 /**
267 * @dataProvider provideSecuritySensitiveOperationStatus
268 * @param bool $mutableSession
269 */
270 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
271 $this->logger = new \Psr\Log\NullLogger();
272 $user = \User::newFromName( 'UTSysop' );
273 $provideUser = null;
274 $reauth = $mutableSession ? AuthManager::SEC_REAUTH : AuthManager::SEC_FAIL;
275
276 list( $provider, $reset ) = $this->getMockSessionProvider(
277 $mutableSession, [ 'provideSessionInfo' ]
278 );
279 $provider->expects( $this->any() )->method( 'provideSessionInfo' )
280 ->will( $this->returnCallback( function () use ( $provider, &$provideUser ) {
281 return new SessionInfo( SessionInfo::MIN_PRIORITY, [
282 'provider' => $provider,
283 'id' => \DummySessionProvider::ID,
284 'persisted' => true,
285 'userInfo' => UserInfo::newFromUser( $provideUser, true )
286 ] );
287 } ) );
288 $this->initializeManager();
289
290 $this->config->set( 'ReauthenticateTime', [] );
291 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [] );
292 $provideUser = new \User;
293 $session = $provider->getManager()->getSessionForRequest( $this->request );
294 $this->assertSame( 0, $session->getUser()->getId(), 'sanity check' );
295
296 // Anonymous user => reauth
297 $session->set( 'AuthManager:lastAuthId', 0 );
298 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
299 $this->assertSame( $reauth, $this->manager->securitySensitiveOperationStatus( 'foo' ) );
300
301 $provideUser = $user;
302 $session = $provider->getManager()->getSessionForRequest( $this->request );
303 $this->assertSame( $user->getId(), $session->getUser()->getId(), 'sanity check' );
304
305 // Error for no default (only gets thrown for non-anonymous user)
306 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
307 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
308 try {
309 $this->manager->securitySensitiveOperationStatus( 'foo' );
310 $this->fail( 'Expected exception not thrown' );
311 } catch ( \UnexpectedValueException $ex ) {
312 $this->assertSame(
313 $mutableSession
314 ? '$wgReauthenticateTime lacks a default'
315 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
316 $ex->getMessage()
317 );
318 }
319
320 if ( $mutableSession ) {
321 $this->config->set( 'ReauthenticateTime', [
322 'test' => 100,
323 'test2' => -1,
324 'default' => 10,
325 ] );
326
327 // Mismatched user ID
328 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
329 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
330 $this->assertSame(
331 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
332 );
333 $this->assertSame(
334 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
335 );
336 $this->assertSame(
337 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
338 );
339
340 // Missing time
341 $session->set( 'AuthManager:lastAuthId', $user->getId() );
342 $session->set( 'AuthManager:lastAuthTimestamp', null );
343 $this->assertSame(
344 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
345 );
346 $this->assertSame(
347 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
348 );
349 $this->assertSame(
350 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
351 );
352
353 // Recent enough to pass
354 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
355 $this->assertSame(
356 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
357 );
358
359 // Not recent enough to pass
360 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
361 $this->assertSame(
362 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
363 );
364 // But recent enough for the 'test' operation
365 $this->assertSame(
366 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
367 );
368 } else {
369 $this->config->set( 'AllowSecuritySensitiveOperationIfCannotReauthenticate', [
370 'test' => false,
371 'default' => true,
372 ] );
373
374 $this->assertEquals(
375 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
376 );
377
378 $this->assertEquals(
379 AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
380 );
381 }
382
383 // Test hook, all three possible values
384 foreach ( [
385 AuthManager::SEC_OK => AuthManager::SEC_OK,
386 AuthManager::SEC_REAUTH => $reauth,
387 AuthManager::SEC_FAIL => AuthManager::SEC_FAIL,
388 ] as $hook => $expect ) {
389 $this->hook( 'SecuritySensitiveOperationStatus', $this->exactly( 2 ) )
390 ->with(
391 $this->anything(),
392 $this->anything(),
393 $this->callback( function ( $s ) use ( $session ) {
394 return $s->getId() === $session->getId();
395 } ),
396 $mutableSession ? $this->equalTo( 500, 1 ) : $this->equalTo( -1 )
397 )
398 ->will( $this->returnCallback( function ( &$v ) use ( $hook ) {
399 $v = $hook;
400 return true;
401 } ) );
402 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
403 $this->assertEquals(
404 $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
405 );
406 $this->assertEquals(
407 $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
408 );
409 $this->unhook( 'SecuritySensitiveOperationStatus' );
410 }
411
412 ScopedCallback::consume( $reset );
413 }
414
415 public function onSecuritySensitiveOperationStatus( &$status, $operation, $session, $time ) {
416 }
417
418 public static function provideSecuritySensitiveOperationStatus() {
419 return [
420 [ true ],
421 [ false ],
422 ];
423 }
424
425 /**
426 * @dataProvider provideUserCanAuthenticate
427 * @param bool $primary1Can
428 * @param bool $primary2Can
429 * @param bool $expect
430 */
431 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
432 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
433 $mock1->expects( $this->any() )->method( 'getUniqueId' )
434 ->will( $this->returnValue( 'primary1' ) );
435 $mock1->expects( $this->any() )->method( 'testUserCanAuthenticate' )
436 ->with( $this->equalTo( 'UTSysop' ) )
437 ->will( $this->returnValue( $primary1Can ) );
438 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
439 $mock2->expects( $this->any() )->method( 'getUniqueId' )
440 ->will( $this->returnValue( 'primary2' ) );
441 $mock2->expects( $this->any() )->method( 'testUserCanAuthenticate' )
442 ->with( $this->equalTo( 'UTSysop' ) )
443 ->will( $this->returnValue( $primary2Can ) );
444 $this->primaryauthMocks = [ $mock1, $mock2 ];
445
446 $this->initializeManager( true );
447 $this->assertSame( $expect, $this->manager->userCanAuthenticate( 'UTSysop' ) );
448 }
449
450 public static function provideUserCanAuthenticate() {
451 return [
452 [ false, false, false ],
453 [ true, false, true ],
454 [ false, true, true ],
455 [ true, true, true ],
456 ];
457 }
458
459 public function testRevokeAccessForUser() {
460 $this->initializeManager();
461
462 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
463 $mock->expects( $this->any() )->method( 'getUniqueId' )
464 ->will( $this->returnValue( 'primary' ) );
465 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
466 ->with( $this->equalTo( 'UTSysop' ) );
467 $this->primaryauthMocks = [ $mock ];
468
469 $this->initializeManager( true );
470 $this->logger->setCollect( true );
471
472 $this->manager->revokeAccessForUser( 'UTSysop' );
473
474 $this->assertSame( [
475 [ LogLevel::INFO, 'Revoking access for {user}' ],
476 ], $this->logger->getBuffer() );
477 }
478
479 public function testProviderCreation() {
480 $mocks = [
481 'pre' => $this->getMockForAbstractClass( PreAuthenticationProvider::class ),
482 'primary' => $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class ),
483 'secondary' => $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class ),
484 ];
485 foreach ( $mocks as $key => $mock ) {
486 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $key ) );
487 $mock->expects( $this->once() )->method( 'setLogger' );
488 $mock->expects( $this->once() )->method( 'setManager' );
489 $mock->expects( $this->once() )->method( 'setConfig' );
490 }
491 $this->preauthMocks = [ $mocks['pre'] ];
492 $this->primaryauthMocks = [ $mocks['primary'] ];
493 $this->secondaryauthMocks = [ $mocks['secondary'] ];
494
495 // Normal operation
496 $this->initializeManager();
497 $this->assertSame(
498 $mocks['primary'],
499 $this->managerPriv->getAuthenticationProvider( 'primary' )
500 );
501 $this->assertSame(
502 $mocks['secondary'],
503 $this->managerPriv->getAuthenticationProvider( 'secondary' )
504 );
505 $this->assertSame(
506 $mocks['pre'],
507 $this->managerPriv->getAuthenticationProvider( 'pre' )
508 );
509 $this->assertSame(
510 [ 'pre' => $mocks['pre'] ],
511 $this->managerPriv->getPreAuthenticationProviders()
512 );
513 $this->assertSame(
514 [ 'primary' => $mocks['primary'] ],
515 $this->managerPriv->getPrimaryAuthenticationProviders()
516 );
517 $this->assertSame(
518 [ 'secondary' => $mocks['secondary'] ],
519 $this->managerPriv->getSecondaryAuthenticationProviders()
520 );
521
522 // Duplicate IDs
523 $mock1 = $this->getMockForAbstractClass( PreAuthenticationProvider::class );
524 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
525 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
526 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
527 $this->preauthMocks = [ $mock1 ];
528 $this->primaryauthMocks = [ $mock2 ];
529 $this->secondaryauthMocks = [];
530 $this->initializeManager( true );
531 try {
532 $this->managerPriv->getAuthenticationProvider( 'Y' );
533 $this->fail( 'Expected exception not thrown' );
534 } catch ( \RuntimeException $ex ) {
535 $class1 = get_class( $mock1 );
536 $class2 = get_class( $mock2 );
537 $this->assertSame(
538 "Duplicate specifications for id X (classes $class1 and $class2)", $ex->getMessage()
539 );
540 }
541
542 // Wrong classes
543 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
544 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
545 $class = get_class( $mock );
546 $this->preauthMocks = [ $mock ];
547 $this->primaryauthMocks = [ $mock ];
548 $this->secondaryauthMocks = [ $mock ];
549 $this->initializeManager( true );
550 try {
551 $this->managerPriv->getPreAuthenticationProviders();
552 $this->fail( 'Expected exception not thrown' );
553 } catch ( \RuntimeException $ex ) {
554 $this->assertSame(
555 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
556 $ex->getMessage()
557 );
558 }
559 try {
560 $this->managerPriv->getPrimaryAuthenticationProviders();
561 $this->fail( 'Expected exception not thrown' );
562 } catch ( \RuntimeException $ex ) {
563 $this->assertSame(
564 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
565 $ex->getMessage()
566 );
567 }
568 try {
569 $this->managerPriv->getSecondaryAuthenticationProviders();
570 $this->fail( 'Expected exception not thrown' );
571 } catch ( \RuntimeException $ex ) {
572 $this->assertSame(
573 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
574 $ex->getMessage()
575 );
576 }
577
578 // Sorting
579 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
580 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
581 $mock3 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
582 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
583 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
584 $mock3->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'C' ) );
585 $this->preauthMocks = [];
586 $this->primaryauthMocks = [ $mock1, $mock2, $mock3 ];
587 $this->secondaryauthMocks = [];
588 $this->initializeConfig();
589 $config = $this->config->get( 'AuthManagerConfig' );
590
591 $this->initializeManager( false );
592 $this->assertSame(
593 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
594 $this->managerPriv->getPrimaryAuthenticationProviders(),
595 'sanity check'
596 );
597
598 $config['primaryauth']['A']['sort'] = 100;
599 $config['primaryauth']['C']['sort'] = -1;
600 $this->config->set( 'AuthManagerConfig', $config );
601 $this->initializeManager( false );
602 $this->assertSame(
603 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
604 $this->managerPriv->getPrimaryAuthenticationProviders()
605 );
606 }
607
608 /**
609 * @dataProvider provideSetDefaultUserOptions
610 */
611 public function testSetDefaultUserOptions(
612 $contLang, $useContextLang, $expectedLang, $expectedVariant
613 ) {
614 $this->initializeManager();
615
616 $this->setContentLang( $contLang );
617 $context = \RequestContext::getMain();
618 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
619 $context->setLanguage( 'de' );
620
621 $user = \User::newFromName( self::usernameForCreation() );
622 $user->addToDatabase();
623 $oldToken = $user->getToken();
624 $this->managerPriv->setDefaultUserOptions( $user, $useContextLang );
625 $user->saveSettings();
626 $this->assertNotEquals( $oldToken, $user->getToken() );
627 $this->assertSame( $expectedLang, $user->getOption( 'language' ) );
628 $this->assertSame( $expectedVariant, $user->getOption( 'variant' ) );
629 }
630
631 public function provideSetDefaultUserOptions() {
632 return [
633 [ 'zh', false, 'zh', 'zh' ],
634 [ 'zh', true, 'de', 'zh' ],
635 [ 'fr', true, 'de', null ],
636 ];
637 }
638
639 public function testForcePrimaryAuthenticationProviders() {
640 $mockA = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
641 $mockB = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
642 $mockB2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
643 $mockA->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'A' ) );
644 $mockB->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
645 $mockB2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'B' ) );
646 $this->primaryauthMocks = [ $mockA ];
647
648 $this->logger = new \TestLogger( true );
649
650 // Test without first initializing the configured providers
651 $this->initializeManager();
652 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
653 $this->assertSame(
654 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
655 );
656 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
657 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
658 $this->assertSame( [
659 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
660 ], $this->logger->getBuffer() );
661 $this->logger->clearBuffer();
662
663 // Test with first initializing the configured providers
664 $this->initializeManager();
665 $this->assertSame( $mockA, $this->managerPriv->getAuthenticationProvider( 'A' ) );
666 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'B' ) );
667 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
668 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
669 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
670 $this->assertSame(
671 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
672 );
673 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
674 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
675 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
676 $this->assertNull(
677 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
678 );
679 $this->assertSame( [
680 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
681 [
682 LogLevel::WARNING,
683 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
684 ],
685 ], $this->logger->getBuffer() );
686 $this->logger->clearBuffer();
687
688 // Test duplicate IDs
689 $this->initializeManager();
690 try {
691 $this->manager->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
692 $this->fail( 'Expected exception not thrown' );
693 } catch ( \RuntimeException $ex ) {
694 $class1 = get_class( $mockB );
695 $class2 = get_class( $mockB2 );
696 $this->assertSame(
697 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
698 );
699 }
700
701 // Wrong classes
702 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
703 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
704 $class = get_class( $mock );
705 try {
706 $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
707 $this->fail( 'Expected exception not thrown' );
708 } catch ( \RuntimeException $ex ) {
709 $this->assertSame(
710 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
711 $ex->getMessage()
712 );
713 }
714 }
715
716 public function testBeginAuthentication() {
717 $this->initializeManager();
718
719 // Immutable session
720 list( $provider, $reset ) = $this->getMockSessionProvider( false );
721 $this->hook( 'UserLoggedIn', $this->never() );
722 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
723 try {
724 $this->manager->beginAuthentication( [], 'http://localhost/' );
725 $this->fail( 'Expected exception not thrown' );
726 } catch ( \LogicException $ex ) {
727 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
728 }
729 $this->unhook( 'UserLoggedIn' );
730 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
731 ScopedCallback::consume( $reset );
732 $this->initializeManager( true );
733
734 // CreatedAccountAuthenticationRequest
735 $user = \User::newFromName( 'UTSysop' );
736 $reqs = [
737 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
738 ];
739 $this->hook( 'UserLoggedIn', $this->never() );
740 try {
741 $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
742 $this->fail( 'Expected exception not thrown' );
743 } catch ( \LogicException $ex ) {
744 $this->assertSame(
745 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
746 'that created the account',
747 $ex->getMessage()
748 );
749 }
750 $this->unhook( 'UserLoggedIn' );
751
752 $this->request->getSession()->clear();
753 $this->request->getSession()->setSecret( 'AuthManager::authnState', 'test' );
754 $this->managerPriv->createdAccountAuthenticationRequests = [ $reqs[0] ];
755 $this->hook( 'UserLoggedIn', $this->once() )
756 ->with( $this->callback( function ( $u ) use ( $user ) {
757 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
758 } ) );
759 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
760 $this->logger->setCollect( true );
761 $ret = $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
762 $this->logger->setCollect( false );
763 $this->unhook( 'UserLoggedIn' );
764 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
765 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
766 $this->assertSame( $user->getName(), $ret->username );
767 $this->assertSame( $user->getId(), $this->request->getSessionData( 'AuthManager:lastAuthId' ) );
768 $this->assertEquals(
769 time(), $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
770 'timestamp ±1', 1
771 );
772 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::authnState' ) );
773 $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
774 $this->assertSame( [
775 [ LogLevel::INFO, 'Logging in {user} after account creation' ],
776 ], $this->logger->getBuffer() );
777 }
778
779 public function testCreateFromLogin() {
780 $user = \User::newFromName( 'UTSysop' );
781 $req1 = $this->createMock( AuthenticationRequest::class );
782 $req2 = $this->createMock( AuthenticationRequest::class );
783 $req3 = $this->createMock( AuthenticationRequest::class );
784 $userReq = new UsernameAuthenticationRequest;
785 $userReq->username = 'UTDummy';
786
787 $req1->returnToUrl = 'http://localhost/';
788 $req2->returnToUrl = 'http://localhost/';
789 $req3->returnToUrl = 'http://localhost/';
790 $req3->username = 'UTDummy';
791 $userReq->returnToUrl = 'http://localhost/';
792
793 // Passing one into beginAuthentication(), and an immediate FAIL
794 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
795 $this->primaryauthMocks = [ $primary ];
796 $this->initializeManager( true );
797 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
798 $res->createRequest = $req1;
799 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
800 ->will( $this->returnValue( $res ) );
801 $createReq = new CreateFromLoginAuthenticationRequest(
802 null, [ $req2->getUniqueId() => $req2 ]
803 );
804 $this->logger->setCollect( true );
805 $ret = $this->manager->beginAuthentication( [ $createReq ], 'http://localhost/' );
806 $this->logger->setCollect( false );
807 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
808 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
809 $this->assertSame( $req1, $ret->createRequest->createRequest );
810 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest->maybeLink );
811
812 // UI, then FAIL in beginAuthentication()
813 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider::class )
814 ->setMethods( [ 'continuePrimaryAuthentication' ] )
815 ->getMockForAbstractClass();
816 $this->primaryauthMocks = [ $primary ];
817 $this->initializeManager( true );
818 $primary->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
819 ->will( $this->returnValue(
820 AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) )
821 ) );
822 $res = AuthenticationResponse::newFail( wfMessage( 'foo' ) );
823 $res->createRequest = $req2;
824 $primary->expects( $this->any() )->method( 'continuePrimaryAuthentication' )
825 ->will( $this->returnValue( $res ) );
826 $this->logger->setCollect( true );
827 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
828 $this->assertSame( AuthenticationResponse::UI, $ret->status, 'sanity check' );
829 $ret = $this->manager->continueAuthentication( [] );
830 $this->logger->setCollect( false );
831 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
832 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest::class, $ret->createRequest );
833 $this->assertSame( $req2, $ret->createRequest->createRequest );
834 $this->assertEquals( [], $ret->createRequest->maybeLink );
835
836 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
837 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider::class );
838 $this->primaryauthMocks = [ $primary ];
839 $this->initializeManager( true );
840 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
841 $createReq->returnToUrl = 'http://localhost/';
842 $createReq->username = 'UTDummy';
843 $res = AuthenticationResponse::newUI( [ $req1 ], wfMessage( 'foo' ) );
844 $primary->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
845 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
846 ->will( $this->returnValue( $res ) );
847 $primary->expects( $this->any() )->method( 'accountCreationType' )
848 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
849 $this->logger->setCollect( true );
850 $ret = $this->manager->beginAccountCreation(
851 $user, [ $userReq, $createReq ], 'http://localhost/'
852 );
853 $this->logger->setCollect( false );
854 $this->assertSame( AuthenticationResponse::UI, $ret->status );
855 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
856 $this->assertNotNull( $state );
857 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
858 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
859 }
860
861 /**
862 * @dataProvider provideAuthentication
863 * @param StatusValue $preResponse
864 * @param array $primaryResponses
865 * @param array $secondaryResponses
866 * @param array $managerResponses
867 * @param bool $link Whether the primary authentication provider is a "link" provider
868 */
869 public function testAuthentication(
870 StatusValue $preResponse, array $primaryResponses, array $secondaryResponses,
871 array $managerResponses, $link = false
872 ) {
873 $this->initializeManager();
874 $user = \User::newFromName( 'UTSysop' );
875 $id = $user->getId();
876 $name = $user->getName();
877
878 // Set up lots of mocks...
879 $req = new RememberMeAuthenticationRequest;
880 $req->rememberMe = (bool)rand( 0, 1 );
881 $req->pre = $preResponse;
882 $req->primary = $primaryResponses;
883 $req->secondary = $secondaryResponses;
884 $mocks = [];
885 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
886 $class = ucfirst( $key ) . 'AuthenticationProvider';
887 $mocks[$key] = $this->getMockForAbstractClass(
888 "MediaWiki\\Auth\\$class", [], "Mock$class"
889 );
890 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
891 ->will( $this->returnValue( $key ) );
892 $mocks[$key . '2'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
893 $mocks[$key . '2']->expects( $this->any() )->method( 'getUniqueId' )
894 ->will( $this->returnValue( $key . '2' ) );
895 $mocks[$key . '3'] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
896 $mocks[$key . '3']->expects( $this->any() )->method( 'getUniqueId' )
897 ->will( $this->returnValue( $key . '3' ) );
898 }
899 foreach ( $mocks as $mock ) {
900 $mock->expects( $this->any() )->method( 'getAuthenticationRequests' )
901 ->will( $this->returnValue( [] ) );
902 }
903
904 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
905 ->will( $this->returnCallback( function ( $reqs ) use ( $req ) {
906 $this->assertContains( $req, $reqs );
907 return $req->pre;
908 } ) );
909
910 $ct = count( $req->primary );
911 $callback = $this->returnCallback( function ( $reqs ) use ( $req ) {
912 $this->assertContains( $req, $reqs );
913 return array_shift( $req->primary );
914 } );
915 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
916 ->method( 'beginPrimaryAuthentication' )
917 ->will( $callback );
918 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
919 ->method( 'continuePrimaryAuthentication' )
920 ->will( $callback );
921 if ( $link ) {
922 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
923 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
924 }
925
926 $ct = count( $req->secondary );
927 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req ) {
928 $this->assertSame( $id, $user->getId() );
929 $this->assertSame( $name, $user->getName() );
930 $this->assertContains( $req, $reqs );
931 return array_shift( $req->secondary );
932 } );
933 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
934 ->method( 'beginSecondaryAuthentication' )
935 ->will( $callback );
936 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
937 ->method( 'continueSecondaryAuthentication' )
938 ->will( $callback );
939
940 $abstain = AuthenticationResponse::newAbstain();
941 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
942 ->will( $this->returnValue( StatusValue::newGood() ) );
943 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
944 ->will( $this->returnValue( $abstain ) );
945 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
946 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
947 ->will( $this->returnValue( $abstain ) );
948 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
949 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
950 ->will( $this->returnValue( $abstain ) );
951 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
952
953 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
954 $this->primaryauthMocks = [ $mocks['primary'], $mocks['primary2'] ];
955 $this->secondaryauthMocks = [
956 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
957 // So linking happens
958 new ConfirmLinkSecondaryAuthenticationProvider,
959 ];
960 $this->initializeManager( true );
961 $this->logger->setCollect( true );
962
963 $constraint = \PHPUnit_Framework_Assert::logicalOr(
964 $this->equalTo( AuthenticationResponse::PASS ),
965 $this->equalTo( AuthenticationResponse::FAIL )
966 );
967 $providers = array_filter(
968 array_merge(
969 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
970 ),
971 function ( $p ) {
972 return is_callable( [ $p, 'expects' ] );
973 }
974 );
975 foreach ( $providers as $p ) {
976 $p->postCalled = false;
977 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
978 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
979 if ( $user !== null ) {
980 $this->assertInstanceOf( \User::class, $user );
981 $this->assertSame( 'UTSysop', $user->getName() );
982 }
983 $this->assertInstanceOf( AuthenticationResponse::class, $response );
984 $this->assertThat( $response->status, $constraint );
985 $p->postCalled = $response->status;
986 } );
987 }
988
989 $session = $this->request->getSession();
990 $session->setRememberUser( !$req->rememberMe );
991
992 foreach ( $managerResponses as $i => $response ) {
993 $success = $response instanceof AuthenticationResponse &&
994 $response->status === AuthenticationResponse::PASS;
995 if ( $success ) {
996 $this->hook( 'UserLoggedIn', $this->once() )
997 ->with( $this->callback( function ( $user ) use ( $id, $name ) {
998 return $user->getId() === $id && $user->getName() === $name;
999 } ) );
1000 } else {
1001 $this->hook( 'UserLoggedIn', $this->never() );
1002 }
1003 if ( $success || (
1004 $response instanceof AuthenticationResponse &&
1005 $response->status === AuthenticationResponse::FAIL &&
1006 $response->message->getKey() !== 'authmanager-authn-not-in-progress' &&
1007 $response->message->getKey() !== 'authmanager-authn-no-primary'
1008 )
1009 ) {
1010 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->once() );
1011 } else {
1012 $this->hook( 'AuthManagerLoginAuthenticateAudit', $this->never() );
1013 }
1014
1015 $ex = null;
1016 try {
1017 if ( !$i ) {
1018 $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1019 } else {
1020 $ret = $this->manager->continueAuthentication( [ $req ] );
1021 }
1022 if ( $response instanceof \Exception ) {
1023 $this->fail( 'Expected exception not thrown', "Response $i" );
1024 }
1025 } catch ( \Exception $ex ) {
1026 if ( !$response instanceof \Exception ) {
1027 throw $ex;
1028 }
1029 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1030 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1031 "Response $i, exception, session state" );
1032 $this->unhook( 'UserLoggedIn' );
1033 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1034 return;
1035 }
1036
1037 $this->unhook( 'UserLoggedIn' );
1038 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1039
1040 $this->assertSame( 'http://localhost/', $req->returnToUrl );
1041
1042 $ret->message = $this->message( $ret->message );
1043 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1044 if ( $success ) {
1045 $this->assertSame( $id, $session->getUser()->getId(),
1046 "Response $i, authn" );
1047 } else {
1048 $this->assertSame( 0, $session->getUser()->getId(),
1049 "Response $i, authn" );
1050 }
1051 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
1052 $this->assertNull( $session->getSecret( 'AuthManager::authnState' ),
1053 "Response $i, session state" );
1054 foreach ( $providers as $p ) {
1055 $this->assertSame( $response->status, $p->postCalled,
1056 "Response $i, post-auth callback called" );
1057 }
1058 } else {
1059 $this->assertNotNull( $session->getSecret( 'AuthManager::authnState' ),
1060 "Response $i, session state" );
1061 foreach ( $ret->neededRequests as $neededReq ) {
1062 $this->assertEquals( AuthManager::ACTION_LOGIN, $neededReq->action,
1063 "Response $i, neededRequest action" );
1064 }
1065 $this->assertEquals(
1066 $ret->neededRequests,
1067 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN_CONTINUE ),
1068 "Response $i, continuation check"
1069 );
1070 foreach ( $providers as $p ) {
1071 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
1072 }
1073 }
1074
1075 $state = $session->getSecret( 'AuthManager::authnState' );
1076 $maybeLink = $state['maybeLink'] ?? [];
1077 if ( $link && $response->status === AuthenticationResponse::RESTART ) {
1078 $this->assertEquals(
1079 $response->createRequest->maybeLink,
1080 $maybeLink,
1081 "Response $i, maybeLink"
1082 );
1083 } else {
1084 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1085 }
1086 }
1087
1088 if ( $success ) {
1089 $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1090 'rememberMe checkbox had effect' );
1091 } else {
1092 $this->assertNotSame( $req->rememberMe, $session->shouldRememberUser(),
1093 'rememberMe checkbox wasn\'t applied' );
1094 }
1095 }
1096
1097 public function provideAuthentication() {
1098 $rememberReq = new RememberMeAuthenticationRequest;
1099 $rememberReq->action = AuthManager::ACTION_LOGIN;
1100
1101 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1102 $req->foobar = 'baz';
1103 $restartResponse = AuthenticationResponse::newRestart(
1104 $this->message( 'authmanager-authn-no-local-user' )
1105 );
1106 $restartResponse->neededRequests = [ $rememberReq ];
1107
1108 $restartResponse2Pass = AuthenticationResponse::newPass( null );
1109 $restartResponse2Pass->linkRequest = $req;
1110 $restartResponse2 = AuthenticationResponse::newRestart(
1111 $this->message( 'authmanager-authn-no-local-user-link' )
1112 );
1113 $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(
1114 null, [ $req->getUniqueId() => $req ]
1115 );
1116 $restartResponse2->createRequest->action = AuthManager::ACTION_LOGIN;
1117 $restartResponse2->neededRequests = [ $rememberReq, $restartResponse2->createRequest ];
1118
1119 $userName = 'UTSysop';
1120
1121 return [
1122 'Failure in pre-auth' => [
1123 StatusValue::newFatal( 'fail-from-pre' ),
1124 [],
1125 [],
1126 [
1127 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
1128 AuthenticationResponse::newFail(
1129 $this->message( 'authmanager-authn-not-in-progress' )
1130 ),
1131 ]
1132 ],
1133 'Failure in primary' => [
1134 StatusValue::newGood(),
1135 $tmp = [
1136 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1137 ],
1138 [],
1139 $tmp
1140 ],
1141 'All primary abstain' => [
1142 StatusValue::newGood(),
1143 [
1144 AuthenticationResponse::newAbstain(),
1145 ],
1146 [],
1147 [
1148 AuthenticationResponse::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1149 ]
1150 ],
1151 'Primary UI, then redirect, then fail' => [
1152 StatusValue::newGood(),
1153 $tmp = [
1154 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1155 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1156 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1157 ],
1158 [],
1159 $tmp
1160 ],
1161 'Primary redirect, then abstain' => [
1162 StatusValue::newGood(),
1163 [
1164 $tmp = AuthenticationResponse::newRedirect(
1165 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1166 ),
1167 AuthenticationResponse::newAbstain(),
1168 ],
1169 [],
1170 [
1171 $tmp,
1172 new \DomainException(
1173 'MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1174 )
1175 ]
1176 ],
1177 'Primary UI, then pass with no local user' => [
1178 StatusValue::newGood(),
1179 [
1180 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1181 AuthenticationResponse::newPass( null ),
1182 ],
1183 [],
1184 [
1185 $tmp,
1186 $restartResponse,
1187 ]
1188 ],
1189 'Primary UI, then pass with no local user (link type)' => [
1190 StatusValue::newGood(),
1191 [
1192 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1193 $restartResponse2Pass,
1194 ],
1195 [],
1196 [
1197 $tmp,
1198 $restartResponse2,
1199 ],
1200 true
1201 ],
1202 'Primary pass with invalid username' => [
1203 StatusValue::newGood(),
1204 [
1205 AuthenticationResponse::newPass( '<>' ),
1206 ],
1207 [],
1208 [
1209 new \DomainException( 'MockPrimaryAuthenticationProvider returned an invalid username: <>' ),
1210 ]
1211 ],
1212 'Secondary fail' => [
1213 StatusValue::newGood(),
1214 [
1215 AuthenticationResponse::newPass( $userName ),
1216 ],
1217 $tmp = [
1218 AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1219 ],
1220 $tmp
1221 ],
1222 'Secondary UI, then abstain' => [
1223 StatusValue::newGood(),
1224 [
1225 AuthenticationResponse::newPass( $userName ),
1226 ],
1227 [
1228 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1229 AuthenticationResponse::newAbstain()
1230 ],
1231 [
1232 $tmp,
1233 AuthenticationResponse::newPass( $userName ),
1234 ]
1235 ],
1236 'Secondary pass' => [
1237 StatusValue::newGood(),
1238 [
1239 AuthenticationResponse::newPass( $userName ),
1240 ],
1241 [
1242 AuthenticationResponse::newPass()
1243 ],
1244 [
1245 AuthenticationResponse::newPass( $userName ),
1246 ]
1247 ],
1248 ];
1249 }
1250
1251 /**
1252 * @dataProvider provideUserExists
1253 * @param bool $primary1Exists
1254 * @param bool $primary2Exists
1255 * @param bool $expect
1256 */
1257 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1258 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1259 $mock1->expects( $this->any() )->method( 'getUniqueId' )
1260 ->will( $this->returnValue( 'primary1' ) );
1261 $mock1->expects( $this->any() )->method( 'testUserExists' )
1262 ->with( $this->equalTo( 'UTSysop' ) )
1263 ->will( $this->returnValue( $primary1Exists ) );
1264 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1265 $mock2->expects( $this->any() )->method( 'getUniqueId' )
1266 ->will( $this->returnValue( 'primary2' ) );
1267 $mock2->expects( $this->any() )->method( 'testUserExists' )
1268 ->with( $this->equalTo( 'UTSysop' ) )
1269 ->will( $this->returnValue( $primary2Exists ) );
1270 $this->primaryauthMocks = [ $mock1, $mock2 ];
1271
1272 $this->initializeManager( true );
1273 $this->assertSame( $expect, $this->manager->userExists( 'UTSysop' ) );
1274 }
1275
1276 public static function provideUserExists() {
1277 return [
1278 [ false, false, false ],
1279 [ true, false, true ],
1280 [ false, true, true ],
1281 [ true, true, true ],
1282 ];
1283 }
1284
1285 /**
1286 * @dataProvider provideAllowsAuthenticationDataChange
1287 * @param StatusValue $primaryReturn
1288 * @param StatusValue $secondaryReturn
1289 * @param Status $expect
1290 */
1291 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1292 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1293
1294 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1295 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1296 $mock1->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1297 ->with( $this->equalTo( $req ) )
1298 ->will( $this->returnValue( $primaryReturn ) );
1299 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
1300 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1301 $mock2->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
1302 ->with( $this->equalTo( $req ) )
1303 ->will( $this->returnValue( $secondaryReturn ) );
1304
1305 $this->primaryauthMocks = [ $mock1 ];
1306 $this->secondaryauthMocks = [ $mock2 ];
1307 $this->initializeManager( true );
1308 $this->assertEquals( $expect, $this->manager->allowsAuthenticationDataChange( $req ) );
1309 }
1310
1311 public static function provideAllowsAuthenticationDataChange() {
1312 $ignored = \Status::newGood( 'ignored' );
1313 $ignored->warning( 'authmanager-change-not-supported' );
1314
1315 $okFromPrimary = StatusValue::newGood();
1316 $okFromPrimary->warning( 'warning-from-primary' );
1317 $okFromSecondary = StatusValue::newGood();
1318 $okFromSecondary->warning( 'warning-from-secondary' );
1319
1320 return [
1321 [
1322 StatusValue::newGood(),
1323 StatusValue::newGood(),
1324 \Status::newGood(),
1325 ],
1326 [
1327 StatusValue::newGood(),
1328 StatusValue::newGood( 'ignore' ),
1329 \Status::newGood(),
1330 ],
1331 [
1332 StatusValue::newGood( 'ignored' ),
1333 StatusValue::newGood(),
1334 \Status::newGood(),
1335 ],
1336 [
1337 StatusValue::newGood( 'ignored' ),
1338 StatusValue::newGood( 'ignored' ),
1339 $ignored,
1340 ],
1341 [
1342 StatusValue::newFatal( 'fail from primary' ),
1343 StatusValue::newGood(),
1344 \Status::newFatal( 'fail from primary' ),
1345 ],
1346 [
1347 $okFromPrimary,
1348 StatusValue::newGood(),
1349 \Status::wrap( $okFromPrimary ),
1350 ],
1351 [
1352 StatusValue::newGood(),
1353 StatusValue::newFatal( 'fail from secondary' ),
1354 \Status::newFatal( 'fail from secondary' ),
1355 ],
1356 [
1357 StatusValue::newGood(),
1358 $okFromSecondary,
1359 \Status::wrap( $okFromSecondary ),
1360 ],
1361 ];
1362 }
1363
1364 public function testChangeAuthenticationData() {
1365 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1366 $req->username = 'UTSysop';
1367
1368 $mock1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1369 $mock1->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '1' ) );
1370 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1371 ->with( $this->equalTo( $req ) );
1372 $mock2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1373 $mock2->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( '2' ) );
1374 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1375 ->with( $this->equalTo( $req ) );
1376
1377 $this->primaryauthMocks = [ $mock1, $mock2 ];
1378 $this->initializeManager( true );
1379 $this->logger->setCollect( true );
1380 $this->manager->changeAuthenticationData( $req );
1381 $this->assertSame( [
1382 [ LogLevel::INFO, 'Changing authentication data for {user} class {what}' ],
1383 ], $this->logger->getBuffer() );
1384 }
1385
1386 public function testCanCreateAccounts() {
1387 $types = [
1388 PrimaryAuthenticationProvider::TYPE_CREATE => true,
1389 PrimaryAuthenticationProvider::TYPE_LINK => true,
1390 PrimaryAuthenticationProvider::TYPE_NONE => false,
1391 ];
1392
1393 foreach ( $types as $type => $can ) {
1394 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1395 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
1396 $mock->expects( $this->any() )->method( 'accountCreationType' )
1397 ->will( $this->returnValue( $type ) );
1398 $this->primaryauthMocks = [ $mock ];
1399 $this->initializeManager( true );
1400 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
1401 }
1402 }
1403
1404 public function testCheckAccountCreatePermissions() {
1405 $this->initializeManager( true );
1406
1407 $this->setGroupPermissions( '*', 'createaccount', true );
1408 $this->assertEquals(
1409 \Status::newGood(),
1410 $this->manager->checkAccountCreatePermissions( new \User )
1411 );
1412
1413 $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1414 $readOnlyMode->setReason( 'Because' );
1415 $this->assertEquals(
1416 \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1417 $this->manager->checkAccountCreatePermissions( new \User )
1418 );
1419 $readOnlyMode->setReason( false );
1420
1421 $this->setGroupPermissions( '*', 'createaccount', false );
1422 $status = $this->manager->checkAccountCreatePermissions( new \User );
1423 $this->assertFalse( $status->isOK() );
1424 $this->assertTrue( $status->hasMessage( 'badaccess-groups' ) );
1425 $this->setGroupPermissions( '*', 'createaccount', true );
1426
1427 $user = \User::newFromName( 'UTBlockee' );
1428 if ( $user->getID() == 0 ) {
1429 $user->addToDatabase();
1430 \TestUser::setPasswordForUser( $user, 'UTBlockeePassword' );
1431 $user->saveSettings();
1432 }
1433 $oldBlock = \Block::newFromTarget( 'UTBlockee' );
1434 if ( $oldBlock ) {
1435 // An old block will prevent our new one from saving.
1436 $oldBlock->delete();
1437 }
1438 $blockOptions = [
1439 'address' => 'UTBlockee',
1440 'user' => $user->getID(),
1441 'by' => $this->getTestSysop()->getUser()->getId(),
1442 'reason' => __METHOD__,
1443 'expiry' => time() + 100500,
1444 'createAccount' => true,
1445 ];
1446 $block = new \Block( $blockOptions );
1447 $block->insert();
1448 $status = $this->manager->checkAccountCreatePermissions( $user );
1449 $this->assertFalse( $status->isOK() );
1450 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-text' ) );
1451
1452 $blockOptions = [
1453 'address' => '127.0.0.0/24',
1454 'by' => $this->getTestSysop()->getUser()->getId(),
1455 'reason' => __METHOD__,
1456 'expiry' => time() + 100500,
1457 'createAccount' => true,
1458 ];
1459 $block = new \Block( $blockOptions );
1460 $block->insert();
1461 $scopeVariable = new ScopedCallback( [ $block, 'delete' ] );
1462 $status = $this->manager->checkAccountCreatePermissions( new \User );
1463 $this->assertFalse( $status->isOK() );
1464 $this->assertTrue( $status->hasMessage( 'cantcreateaccount-range-text' ) );
1465 ScopedCallback::consume( $scopeVariable );
1466
1467 $this->setMwGlobals( [
1468 'wgEnableDnsBlacklist' => true,
1469 'wgDnsBlacklistUrls' => [
1470 'local.wmftest.net', // This will resolve for every subdomain, which works to test "listed?"
1471 ],
1472 'wgProxyWhitelist' => [],
1473 ] );
1474 $status = $this->manager->checkAccountCreatePermissions( new \User );
1475 $this->assertFalse( $status->isOK() );
1476 $this->assertTrue( $status->hasMessage( 'sorbs_create_account_reason' ) );
1477 $this->setMwGlobals( 'wgProxyWhitelist', [ '127.0.0.1' ] );
1478 $status = $this->manager->checkAccountCreatePermissions( new \User );
1479 $this->assertTrue( $status->isGood() );
1480 }
1481
1482 /**
1483 * @param string $uniq
1484 * @return string
1485 */
1486 private static function usernameForCreation( $uniq = '' ) {
1487 $i = 0;
1488 do {
1489 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1490 } while ( \User::newFromName( $username )->getId() !== 0 );
1491 return $username;
1492 }
1493
1494 public function testCanCreateAccount() {
1495 $username = self::usernameForCreation();
1496 $this->initializeManager();
1497
1498 $this->assertEquals(
1499 \Status::newFatal( 'authmanager-create-disabled' ),
1500 $this->manager->canCreateAccount( $username )
1501 );
1502
1503 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1504 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1505 $mock->expects( $this->any() )->method( 'accountCreationType' )
1506 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1507 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1508 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1509 ->will( $this->returnValue( StatusValue::newGood() ) );
1510 $this->primaryauthMocks = [ $mock ];
1511 $this->initializeManager( true );
1512
1513 $this->assertEquals(
1514 \Status::newFatal( 'userexists' ),
1515 $this->manager->canCreateAccount( $username )
1516 );
1517
1518 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1519 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1520 $mock->expects( $this->any() )->method( 'accountCreationType' )
1521 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1522 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1523 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1524 ->will( $this->returnValue( StatusValue::newGood() ) );
1525 $this->primaryauthMocks = [ $mock ];
1526 $this->initializeManager( true );
1527
1528 $this->assertEquals(
1529 \Status::newFatal( 'noname' ),
1530 $this->manager->canCreateAccount( $username . '<>' )
1531 );
1532
1533 $this->assertEquals(
1534 \Status::newFatal( 'userexists' ),
1535 $this->manager->canCreateAccount( 'UTSysop' )
1536 );
1537
1538 $this->assertEquals(
1539 \Status::newGood(),
1540 $this->manager->canCreateAccount( $username )
1541 );
1542
1543 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1544 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1545 $mock->expects( $this->any() )->method( 'accountCreationType' )
1546 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1547 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1548 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1549 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1550 $this->primaryauthMocks = [ $mock ];
1551 $this->initializeManager( true );
1552
1553 $this->assertEquals(
1554 \Status::newFatal( 'fail' ),
1555 $this->manager->canCreateAccount( $username )
1556 );
1557 }
1558
1559 public function testBeginAccountCreation() {
1560 $creator = \User::newFromName( 'UTSysop' );
1561 $userReq = new UsernameAuthenticationRequest;
1562 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1563 return $level === LogLevel::DEBUG ? null : $message;
1564 } );
1565 $this->initializeManager();
1566
1567 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', 'test' );
1568 $this->hook( 'LocalUserCreated', $this->never() );
1569 try {
1570 $this->manager->beginAccountCreation(
1571 $creator, [], 'http://localhost/'
1572 );
1573 $this->fail( 'Expected exception not thrown' );
1574 } catch ( \LogicException $ex ) {
1575 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1576 }
1577 $this->unhook( 'LocalUserCreated' );
1578 $this->assertNull(
1579 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1580 );
1581
1582 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1583 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1584 $mock->expects( $this->any() )->method( 'accountCreationType' )
1585 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1586 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
1587 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1588 ->will( $this->returnValue( StatusValue::newGood() ) );
1589 $this->primaryauthMocks = [ $mock ];
1590 $this->initializeManager( true );
1591
1592 $this->hook( 'LocalUserCreated', $this->never() );
1593 $ret = $this->manager->beginAccountCreation( $creator, [], 'http://localhost/' );
1594 $this->unhook( 'LocalUserCreated' );
1595 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1596 $this->assertSame( 'noname', $ret->message->getKey() );
1597
1598 $this->hook( 'LocalUserCreated', $this->never() );
1599 $userReq->username = self::usernameForCreation();
1600 $userReq2 = new UsernameAuthenticationRequest;
1601 $userReq2->username = $userReq->username . 'X';
1602 $ret = $this->manager->beginAccountCreation(
1603 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
1604 );
1605 $this->unhook( 'LocalUserCreated' );
1606 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1607 $this->assertSame( 'noname', $ret->message->getKey() );
1608
1609 $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1610 $readOnlyMode->setReason( 'Because' );
1611 $this->hook( 'LocalUserCreated', $this->never() );
1612 $userReq->username = self::usernameForCreation();
1613 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1614 $this->unhook( 'LocalUserCreated' );
1615 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1616 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1617 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1618 $readOnlyMode->setReason( false );
1619
1620 $this->hook( 'LocalUserCreated', $this->never() );
1621 $userReq->username = self::usernameForCreation();
1622 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1623 $this->unhook( 'LocalUserCreated' );
1624 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1625 $this->assertSame( 'userexists', $ret->message->getKey() );
1626
1627 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1628 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1629 $mock->expects( $this->any() )->method( 'accountCreationType' )
1630 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1631 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1632 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1633 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1634 $this->primaryauthMocks = [ $mock ];
1635 $this->initializeManager( true );
1636
1637 $this->hook( 'LocalUserCreated', $this->never() );
1638 $userReq->username = self::usernameForCreation();
1639 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1640 $this->unhook( 'LocalUserCreated' );
1641 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1642 $this->assertSame( 'fail', $ret->message->getKey() );
1643
1644 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1645 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1646 $mock->expects( $this->any() )->method( 'accountCreationType' )
1647 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1648 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1649 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1650 ->will( $this->returnValue( StatusValue::newGood() ) );
1651 $this->primaryauthMocks = [ $mock ];
1652 $this->initializeManager( true );
1653
1654 $this->hook( 'LocalUserCreated', $this->never() );
1655 $userReq->username = self::usernameForCreation() . '<>';
1656 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1657 $this->unhook( 'LocalUserCreated' );
1658 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1659 $this->assertSame( 'noname', $ret->message->getKey() );
1660
1661 $this->hook( 'LocalUserCreated', $this->never() );
1662 $userReq->username = $creator->getName();
1663 $ret = $this->manager->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
1664 $this->unhook( 'LocalUserCreated' );
1665 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1666 $this->assertSame( 'userexists', $ret->message->getKey() );
1667
1668 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1669 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1670 $mock->expects( $this->any() )->method( 'accountCreationType' )
1671 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1672 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1673 $mock->expects( $this->any() )->method( 'testUserForCreation' )
1674 ->will( $this->returnValue( StatusValue::newGood() ) );
1675 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
1676 ->will( $this->returnValue( StatusValue::newFatal( 'fail' ) ) );
1677 $this->primaryauthMocks = [ $mock ];
1678 $this->initializeManager( true );
1679
1680 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1681 ->setMethods( [ 'populateUser' ] )
1682 ->getMock();
1683 $req->expects( $this->any() )->method( 'populateUser' )
1684 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1685 $userReq->username = self::usernameForCreation();
1686 $ret = $this->manager->beginAccountCreation(
1687 $creator, [ $userReq, $req ], 'http://localhost/'
1688 );
1689 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1690 $this->assertSame( 'populatefail', $ret->message->getKey() );
1691
1692 $req = new UserDataAuthenticationRequest;
1693 $userReq->username = self::usernameForCreation();
1694
1695 $ret = $this->manager->beginAccountCreation(
1696 $creator, [ $userReq, $req ], 'http://localhost/'
1697 );
1698 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1699 $this->assertSame( 'fail', $ret->message->getKey() );
1700
1701 $this->manager->beginAccountCreation(
1702 \User::newFromName( $userReq->username ), [ $userReq, $req ], 'http://localhost/'
1703 );
1704 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1705 $this->assertSame( 'fail', $ret->message->getKey() );
1706 }
1707
1708 public function testContinueAccountCreation() {
1709 $creator = \User::newFromName( 'UTSysop' );
1710 $username = self::usernameForCreation();
1711 $this->logger = new \TestLogger( false, function ( $message, $level ) {
1712 return $level === LogLevel::DEBUG ? null : $message;
1713 } );
1714 $this->initializeManager();
1715
1716 $session = [
1717 'userid' => 0,
1718 'username' => $username,
1719 'creatorid' => 0,
1720 'creatorname' => $username,
1721 'reqs' => [],
1722 'primary' => null,
1723 'primaryResponse' => null,
1724 'secondary' => [],
1725 'ranPreTests' => true,
1726 ];
1727
1728 $this->hook( 'LocalUserCreated', $this->never() );
1729 try {
1730 $this->manager->continueAccountCreation( [] );
1731 $this->fail( 'Expected exception not thrown' );
1732 } catch ( \LogicException $ex ) {
1733 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1734 }
1735 $this->unhook( 'LocalUserCreated' );
1736
1737 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
1738 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
1739 $mock->expects( $this->any() )->method( 'accountCreationType' )
1740 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1741 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( false ) );
1742 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )->will(
1743 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
1744 );
1745 $this->primaryauthMocks = [ $mock ];
1746 $this->initializeManager( true );
1747
1748 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', null );
1749 $this->hook( 'LocalUserCreated', $this->never() );
1750 $ret = $this->manager->continueAccountCreation( [] );
1751 $this->unhook( 'LocalUserCreated' );
1752 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1753 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message->getKey() );
1754
1755 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1756 [ 'username' => "$username<>" ] + $session );
1757 $this->hook( 'LocalUserCreated', $this->never() );
1758 $ret = $this->manager->continueAccountCreation( [] );
1759 $this->unhook( 'LocalUserCreated' );
1760 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1761 $this->assertSame( 'noname', $ret->message->getKey() );
1762 $this->assertNull(
1763 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1764 );
1765
1766 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $session );
1767 $this->hook( 'LocalUserCreated', $this->never() );
1768 $cache = \ObjectCache::getLocalClusterInstance();
1769 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1770 $ret = $this->manager->continueAccountCreation( [] );
1771 unset( $lock );
1772 $this->unhook( 'LocalUserCreated' );
1773 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1774 $this->assertSame( 'usernameinprogress', $ret->message->getKey() );
1775 // This error shouldn't remove the existing session, because the
1776 // raced-with process "owns" it.
1777 $this->assertSame(
1778 $session, $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1779 );
1780
1781 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1782 [ 'username' => $creator->getName() ] + $session );
1783 $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1784 $readOnlyMode->setReason( 'Because' );
1785 $this->hook( 'LocalUserCreated', $this->never() );
1786 $ret = $this->manager->continueAccountCreation( [] );
1787 $this->unhook( 'LocalUserCreated' );
1788 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1789 $this->assertSame( 'readonlytext', $ret->message->getKey() );
1790 $this->assertSame( [ 'Because' ], $ret->message->getParams() );
1791 $readOnlyMode->setReason( false );
1792
1793 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1794 [ 'username' => $creator->getName() ] + $session );
1795 $this->hook( 'LocalUserCreated', $this->never() );
1796 $ret = $this->manager->continueAccountCreation( [] );
1797 $this->unhook( 'LocalUserCreated' );
1798 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1799 $this->assertSame( 'userexists', $ret->message->getKey() );
1800 $this->assertNull(
1801 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1802 );
1803
1804 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1805 [ 'userid' => $creator->getId() ] + $session );
1806 $this->hook( 'LocalUserCreated', $this->never() );
1807 try {
1808 $ret = $this->manager->continueAccountCreation( [] );
1809 $this->fail( 'Expected exception not thrown' );
1810 } catch ( \UnexpectedValueException $ex ) {
1811 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
1812 }
1813 $this->unhook( 'LocalUserCreated' );
1814 $this->assertNull(
1815 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1816 );
1817
1818 $id = $creator->getId();
1819 $name = $creator->getName();
1820 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1821 [ 'username' => $name, 'userid' => $id + 1 ] + $session );
1822 $this->hook( 'LocalUserCreated', $this->never() );
1823 try {
1824 $ret = $this->manager->continueAccountCreation( [] );
1825 $this->fail( 'Expected exception not thrown' );
1826 } catch ( \UnexpectedValueException $ex ) {
1827 $this->assertEquals(
1828 "User \"{$name}\" exists, but ID $id !== " . ( $id + 1 ) . '!', $ex->getMessage()
1829 );
1830 }
1831 $this->unhook( 'LocalUserCreated' );
1832 $this->assertNull(
1833 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1834 );
1835
1836 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
1837 ->setMethods( [ 'populateUser' ] )
1838 ->getMock();
1839 $req->expects( $this->any() )->method( 'populateUser' )
1840 ->willReturn( \StatusValue::newFatal( 'populatefail' ) );
1841 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState',
1842 [ 'reqs' => [ $req ] ] + $session );
1843 $ret = $this->manager->continueAccountCreation( [] );
1844 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
1845 $this->assertSame( 'populatefail', $ret->message->getKey() );
1846 $this->assertNull(
1847 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' )
1848 );
1849 }
1850
1851 /**
1852 * @dataProvider provideAccountCreation
1853 * @param StatusValue $preTest
1854 * @param StatusValue $primaryTest
1855 * @param StatusValue $secondaryTest
1856 * @param array $primaryResponses
1857 * @param array $secondaryResponses
1858 * @param array $managerResponses
1859 */
1860 public function testAccountCreation(
1861 StatusValue $preTest, $primaryTest, $secondaryTest,
1862 array $primaryResponses, array $secondaryResponses, array $managerResponses
1863 ) {
1864 $creator = \User::newFromName( 'UTSysop' );
1865 $username = self::usernameForCreation();
1866
1867 $this->initializeManager();
1868
1869 // Set up lots of mocks...
1870 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
1871 $req->preTest = $preTest;
1872 $req->primaryTest = $primaryTest;
1873 $req->secondaryTest = $secondaryTest;
1874 $req->primary = $primaryResponses;
1875 $req->secondary = $secondaryResponses;
1876 $mocks = [];
1877 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1878 $class = ucfirst( $key ) . 'AuthenticationProvider';
1879 $mocks[$key] = $this->getMockForAbstractClass(
1880 "MediaWiki\\Auth\\$class", [], "Mock$class"
1881 );
1882 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
1883 ->will( $this->returnValue( $key ) );
1884 $mocks[$key]->expects( $this->any() )->method( 'testUserForCreation' )
1885 ->will( $this->returnValue( StatusValue::newGood() ) );
1886 $mocks[$key]->expects( $this->any() )->method( 'testForAccountCreation' )
1887 ->will( $this->returnCallback(
1888 function ( $user, $creatorIn, $reqs )
1889 use ( $username, $creator, $req, $key )
1890 {
1891 $this->assertSame( $username, $user->getName() );
1892 $this->assertSame( $creator->getId(), $creatorIn->getId() );
1893 $this->assertSame( $creator->getName(), $creatorIn->getName() );
1894 $foundReq = false;
1895 foreach ( $reqs as $r ) {
1896 $this->assertSame( $username, $r->username );
1897 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1898 }
1899 $this->assertTrue( $foundReq, '$reqs contains $req' );
1900 $k = $key . 'Test';
1901 return $req->$k;
1902 }
1903 ) );
1904
1905 for ( $i = 2; $i <= 3; $i++ ) {
1906 $mocks[$key . $i] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
1907 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
1908 ->will( $this->returnValue( $key . $i ) );
1909 $mocks[$key . $i]->expects( $this->any() )->method( 'testUserForCreation' )
1910 ->will( $this->returnValue( StatusValue::newGood() ) );
1911 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
1912 ->will( $this->returnValue( StatusValue::newGood() ) );
1913 }
1914 }
1915
1916 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
1917 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
1918 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
1919 ->will( $this->returnValue( false ) );
1920 $ct = count( $req->primary );
1921 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1922 $this->assertSame( $username, $user->getName() );
1923 $this->assertSame( 'UTSysop', $creator->getName() );
1924 $foundReq = false;
1925 foreach ( $reqs as $r ) {
1926 $this->assertSame( $username, $r->username );
1927 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1928 }
1929 $this->assertTrue( $foundReq, '$reqs contains $req' );
1930 return array_shift( $req->primary );
1931 } );
1932 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1933 ->method( 'beginPrimaryAccountCreation' )
1934 ->will( $callback );
1935 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1936 ->method( 'continuePrimaryAccountCreation' )
1937 ->will( $callback );
1938
1939 $ct = count( $req->secondary );
1940 $callback = $this->returnCallback( function ( $user, $creator, $reqs ) use ( $username, $req ) {
1941 $this->assertSame( $username, $user->getName() );
1942 $this->assertSame( 'UTSysop', $creator->getName() );
1943 $foundReq = false;
1944 foreach ( $reqs as $r ) {
1945 $this->assertSame( $username, $r->username );
1946 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
1947 }
1948 $this->assertTrue( $foundReq, '$reqs contains $req' );
1949 return array_shift( $req->secondary );
1950 } );
1951 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1952 ->method( 'beginSecondaryAccountCreation' )
1953 ->will( $callback );
1954 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1955 ->method( 'continueSecondaryAccountCreation' )
1956 ->will( $callback );
1957
1958 $abstain = AuthenticationResponse::newAbstain();
1959 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
1960 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
1961 $mocks['primary2']->expects( $this->any() )->method( 'testUserExists' )
1962 ->will( $this->returnValue( false ) );
1963 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
1964 ->will( $this->returnValue( $abstain ) );
1965 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1966 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
1967 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_NONE ) );
1968 $mocks['primary3']->expects( $this->any() )->method( 'testUserExists' )
1969 ->will( $this->returnValue( false ) );
1970 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
1971 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
1972 $mocks['secondary2']->expects( $this->atMost( 1 ) )
1973 ->method( 'beginSecondaryAccountCreation' )
1974 ->will( $this->returnValue( $abstain ) );
1975 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1976 $mocks['secondary3']->expects( $this->atMost( 1 ) )
1977 ->method( 'beginSecondaryAccountCreation' )
1978 ->will( $this->returnValue( $abstain ) );
1979 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
1980
1981 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
1982 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
1983 $this->secondaryauthMocks = [
1984 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
1985 ];
1986
1987 $this->logger = new \TestLogger( true, function ( $message, $level ) {
1988 return $level === LogLevel::DEBUG ? null : $message;
1989 } );
1990 $expectLog = [];
1991 $this->initializeManager( true );
1992
1993 $constraint = \PHPUnit_Framework_Assert::logicalOr(
1994 $this->equalTo( AuthenticationResponse::PASS ),
1995 $this->equalTo( AuthenticationResponse::FAIL )
1996 );
1997 $providers = array_merge(
1998 $this->preauthMocks, $this->primaryauthMocks, $this->secondaryauthMocks
1999 );
2000 foreach ( $providers as $p ) {
2001 $p->postCalled = false;
2002 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2003 ->willReturnCallback( function ( $user, $creator, $response )
2004 use ( $constraint, $p, $username )
2005 {
2006 $this->assertInstanceOf( \User::class, $user );
2007 $this->assertSame( $username, $user->getName() );
2008 $this->assertSame( 'UTSysop', $creator->getName() );
2009 $this->assertInstanceOf( AuthenticationResponse::class, $response );
2010 $this->assertThat( $response->status, $constraint );
2011 $p->postCalled = $response->status;
2012 } );
2013 }
2014
2015 // We're testing with $wgNewUserLog = false, so assert that it worked
2016 $dbw = wfGetDB( DB_MASTER );
2017 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2018
2019 $first = true;
2020 $created = false;
2021 foreach ( $managerResponses as $i => $response ) {
2022 $success = $response instanceof AuthenticationResponse &&
2023 $response->status === AuthenticationResponse::PASS;
2024 if ( $i === 'created' ) {
2025 $created = true;
2026 $this->hook( 'LocalUserCreated', $this->once() )
2027 ->with(
2028 $this->callback( function ( $user ) use ( $username ) {
2029 return $user->getName() === $username;
2030 } ),
2031 $this->equalTo( false )
2032 );
2033 $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2034 } else {
2035 $this->hook( 'LocalUserCreated', $this->never() );
2036 }
2037
2038 $ex = null;
2039 try {
2040 if ( $first ) {
2041 $userReq = new UsernameAuthenticationRequest;
2042 $userReq->username = $username;
2043 $ret = $this->manager->beginAccountCreation(
2044 $creator, [ $userReq, $req ], 'http://localhost/'
2045 );
2046 } else {
2047 $ret = $this->manager->continueAccountCreation( [ $req ] );
2048 }
2049 if ( $response instanceof \Exception ) {
2050 $this->fail( 'Expected exception not thrown', "Response $i" );
2051 }
2052 } catch ( \Exception $ex ) {
2053 if ( !$response instanceof \Exception ) {
2054 throw $ex;
2055 }
2056 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2057 $this->assertNull(
2058 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2059 "Response $i, exception, session state"
2060 );
2061 $this->unhook( 'LocalUserCreated' );
2062 return;
2063 }
2064
2065 $this->unhook( 'LocalUserCreated' );
2066
2067 $this->assertSame( 'http://localhost/', $req->returnToUrl );
2068
2069 if ( $success ) {
2070 $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2071 $this->assertContains(
2072 $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2073 "Response $i, login marker"
2074 );
2075
2076 $expectLog[] = [
2077 LogLevel::INFO,
2078 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2079 ];
2080
2081 // Set some fields in the expected $response that we couldn't
2082 // know in provideAccountCreation().
2083 $response->username = $username;
2084 $response->loginRequest = $ret->loginRequest;
2085 } else {
2086 $this->assertNull( $ret->loginRequest, "Response $i, login marker" );
2087 $this->assertSame( [], $this->managerPriv->createdAccountAuthenticationRequests,
2088 "Response $i, login marker" );
2089 }
2090 $ret->message = $this->message( $ret->message );
2091 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2092 if ( $success || $response->status === AuthenticationResponse::FAIL ) {
2093 $this->assertNull(
2094 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2095 "Response $i, session state"
2096 );
2097 foreach ( $providers as $p ) {
2098 $this->assertSame( $response->status, $p->postCalled,
2099 "Response $i, post-auth callback called" );
2100 }
2101 } else {
2102 $this->assertNotNull(
2103 $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' ),
2104 "Response $i, session state"
2105 );
2106 foreach ( $ret->neededRequests as $neededReq ) {
2107 $this->assertEquals( AuthManager::ACTION_CREATE, $neededReq->action,
2108 "Response $i, neededRequest action" );
2109 }
2110 $this->assertEquals(
2111 $ret->neededRequests,
2112 $this->manager->getAuthenticationRequests( AuthManager::ACTION_CREATE_CONTINUE ),
2113 "Response $i, continuation check"
2114 );
2115 foreach ( $providers as $p ) {
2116 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
2117 }
2118 }
2119
2120 if ( $created ) {
2121 $this->assertNotEquals( 0, \User::idFromName( $username ) );
2122 } else {
2123 $this->assertEquals( 0, \User::idFromName( $username ) );
2124 }
2125
2126 $first = false;
2127 }
2128
2129 $this->assertSame( $expectLog, $this->logger->getBuffer() );
2130
2131 $this->assertSame(
2132 $maxLogId,
2133 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2134 );
2135 }
2136
2137 public function provideAccountCreation() {
2138 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2139 $good = StatusValue::newGood();
2140
2141 return [
2142 'Pre-creation test fail in pre' => [
2143 StatusValue::newFatal( 'fail-from-pre' ), $good, $good,
2144 [],
2145 [],
2146 [
2147 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
2148 ]
2149 ],
2150 'Pre-creation test fail in primary' => [
2151 $good, StatusValue::newFatal( 'fail-from-primary' ), $good,
2152 [],
2153 [],
2154 [
2155 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2156 ]
2157 ],
2158 'Pre-creation test fail in secondary' => [
2159 $good, $good, StatusValue::newFatal( 'fail-from-secondary' ),
2160 [],
2161 [],
2162 [
2163 AuthenticationResponse::newFail( $this->message( 'fail-from-secondary' ) ),
2164 ]
2165 ],
2166 'Failure in primary' => [
2167 $good, $good, $good,
2168 $tmp = [
2169 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2170 ],
2171 [],
2172 $tmp
2173 ],
2174 'All primary abstain' => [
2175 $good, $good, $good,
2176 [
2177 AuthenticationResponse::newAbstain(),
2178 ],
2179 [],
2180 [
2181 AuthenticationResponse::newFail( $this->message( 'authmanager-create-no-primary' ) )
2182 ]
2183 ],
2184 'Primary UI, then redirect, then fail' => [
2185 $good, $good, $good,
2186 $tmp = [
2187 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2188 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2189 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2190 ],
2191 [],
2192 $tmp
2193 ],
2194 'Primary redirect, then abstain' => [
2195 $good, $good, $good,
2196 [
2197 $tmp = AuthenticationResponse::newRedirect(
2198 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2199 ),
2200 AuthenticationResponse::newAbstain(),
2201 ],
2202 [],
2203 [
2204 $tmp,
2205 new \DomainException(
2206 'MockPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2207 )
2208 ]
2209 ],
2210 'Primary UI, then pass; secondary abstain' => [
2211 $good, $good, $good,
2212 [
2213 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2214 AuthenticationResponse::newPass(),
2215 ],
2216 [
2217 AuthenticationResponse::newAbstain(),
2218 ],
2219 [
2220 $tmp1,
2221 'created' => AuthenticationResponse::newPass( '' ),
2222 ]
2223 ],
2224 'Primary pass; secondary UI then pass' => [
2225 $good, $good, $good,
2226 [
2227 AuthenticationResponse::newPass( '' ),
2228 ],
2229 [
2230 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2231 AuthenticationResponse::newPass( '' ),
2232 ],
2233 [
2234 'created' => $tmp1,
2235 AuthenticationResponse::newPass( '' ),
2236 ]
2237 ],
2238 'Primary pass; secondary fail' => [
2239 $good, $good, $good,
2240 [
2241 AuthenticationResponse::newPass(),
2242 ],
2243 [
2244 AuthenticationResponse::newFail( $this->message( '...' ) ),
2245 ],
2246 [
2247 'created' => new \DomainException(
2248 'MockSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2249 'Secondary providers are not allowed to fail account creation, ' .
2250 'that should have been done via testForAccountCreation().'
2251 )
2252 ]
2253 ],
2254 ];
2255 }
2256
2257 /**
2258 * @dataProvider provideAccountCreationLogging
2259 * @param bool $isAnon
2260 * @param string|null $logSubtype
2261 */
2262 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2263 $creator = $isAnon ? new \User : \User::newFromName( 'UTSysop' );
2264 $username = self::usernameForCreation();
2265
2266 $this->initializeManager();
2267
2268 // Set up lots of mocks...
2269 $mock = $this->getMockForAbstractClass(
2270 \MediaWiki\Auth\PrimaryAuthenticationProvider::class, []
2271 );
2272 $mock->expects( $this->any() )->method( 'getUniqueId' )
2273 ->will( $this->returnValue( 'primary' ) );
2274 $mock->expects( $this->any() )->method( 'testUserForCreation' )
2275 ->will( $this->returnValue( StatusValue::newGood() ) );
2276 $mock->expects( $this->any() )->method( 'testForAccountCreation' )
2277 ->will( $this->returnValue( StatusValue::newGood() ) );
2278 $mock->expects( $this->any() )->method( 'accountCreationType' )
2279 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2280 $mock->expects( $this->any() )->method( 'testUserExists' )
2281 ->will( $this->returnValue( false ) );
2282 $mock->expects( $this->any() )->method( 'beginPrimaryAccountCreation' )
2283 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
2284 $mock->expects( $this->any() )->method( 'finishAccountCreation' )
2285 ->will( $this->returnValue( $logSubtype ) );
2286
2287 $this->primaryauthMocks = [ $mock ];
2288 $this->initializeManager( true );
2289 $this->logger->setCollect( true );
2290
2291 $this->config->set( 'NewUserLog', true );
2292
2293 $dbw = wfGetDB( DB_MASTER );
2294 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2295
2296 $userReq = new UsernameAuthenticationRequest;
2297 $userReq->username = $username;
2298 $reasonReq = new CreationReasonAuthenticationRequest;
2299 $reasonReq->reason = $this->toString();
2300 $ret = $this->manager->beginAccountCreation(
2301 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2302 );
2303
2304 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
2305
2306 $user = \User::newFromName( $username );
2307 $this->assertNotEquals( 0, $user->getId(), 'sanity check' );
2308 $this->assertNotEquals( $creator->getId(), $user->getId(), 'sanity check' );
2309
2310 $data = \DatabaseLogEntry::getSelectQueryData();
2311 $rows = iterator_to_array( $dbw->select(
2312 $data['tables'],
2313 $data['fields'],
2314 [
2315 'log_id > ' . (int)$maxLogId,
2316 'log_type' => 'newusers'
2317 ] + $data['conds'],
2318 __METHOD__,
2319 $data['options'],
2320 $data['join_conds']
2321 ) );
2322 $this->assertCount( 1, $rows );
2323 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2324
2325 $this->assertSame( $logSubtype ?: ( $isAnon ? 'create' : 'create2' ), $entry->getSubtype() );
2326 $this->assertSame(
2327 $isAnon ? $user->getId() : $creator->getId(),
2328 $entry->getPerformer()->getId()
2329 );
2330 $this->assertSame(
2331 $isAnon ? $user->getName() : $creator->getName(),
2332 $entry->getPerformer()->getName()
2333 );
2334 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2335 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2336 $this->assertSame( $this->toString(), $entry->getComment() );
2337 }
2338
2339 public static function provideAccountCreationLogging() {
2340 return [
2341 [ true, null ],
2342 [ true, 'foobar' ],
2343 [ false, null ],
2344 [ false, 'byemail' ],
2345 ];
2346 }
2347
2348 public function testAutoAccountCreation() {
2349 // PHPUnit seems to have a bug where it will call the ->with()
2350 // callbacks for our hooks again after the test is run (WTF?), which
2351 // breaks here because $username no longer matches $user by the end of
2352 // the testing.
2353 $workaroundPHPUnitBug = false;
2354
2355 $username = self::usernameForCreation();
2356 $expectedSource = AuthManager::AUTOCREATE_SOURCE_SESSION;
2357 $this->initializeManager();
2358
2359 $this->setGroupPermissions( '*', 'createaccount', true );
2360 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2361
2362 $this->mergeMwGlobalArrayValue( 'wgObjectCaches',
2363 [ __METHOD__ => [ 'class' => 'HashBagOStuff' ] ] );
2364 $this->setMwGlobals( [ 'wgMainCacheType' => __METHOD__ ] );
2365
2366 // Set up lots of mocks...
2367 $mocks = [];
2368 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2369 $class = ucfirst( $key ) . 'AuthenticationProvider';
2370 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
2371 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2372 ->will( $this->returnValue( $key ) );
2373 }
2374
2375 $good = StatusValue::newGood();
2376 $ok = StatusValue::newFatal( 'ok' );
2377 $callback = $this->callback( function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2378 return $workaroundPHPUnitBug || $user->getName() === $username;
2379 } );
2380 $callback2 = $this->callback(
2381 function ( $source ) use ( &$expectedSource, &$workaroundPHPUnitBug ) {
2382 return $workaroundPHPUnitBug || $source === $expectedSource;
2383 }
2384 );
2385
2386 $mocks['pre']->expects( $this->exactly( 13 ) )->method( 'testUserForCreation' )
2387 ->with( $callback, $callback2 )
2388 ->will( $this->onConsecutiveCalls(
2389 $ok, $ok, $ok, // For testing permissions
2390 StatusValue::newFatal( 'fail-in-pre' ), $good, $good,
2391 $good, // backoff test
2392 $good, // addToDatabase fails test
2393 $good, // addToDatabase throws test
2394 $good, // addToDatabase exists test
2395 $good, $good, $good // success
2396 ) );
2397
2398 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
2399 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
2400 $mocks['primary']->expects( $this->any() )->method( 'testUserExists' )
2401 ->will( $this->returnValue( true ) );
2402 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2403 ->with( $callback, $callback2 )
2404 ->will( $this->onConsecutiveCalls(
2405 StatusValue::newFatal( 'fail-in-primary' ), $good,
2406 $good, // backoff test
2407 $good, // addToDatabase fails test
2408 $good, // addToDatabase throws test
2409 $good, // addToDatabase exists test
2410 $good, $good, $good
2411 ) );
2412 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2413 ->with( $callback, $callback2 );
2414
2415 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2416 ->with( $callback, $callback2 )
2417 ->will( $this->onConsecutiveCalls(
2418 StatusValue::newFatal( 'fail-in-secondary' ),
2419 $good, // backoff test
2420 $good, // addToDatabase fails test
2421 $good, // addToDatabase throws test
2422 $good, // addToDatabase exists test
2423 $good, $good, $good
2424 ) );
2425 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2426 ->with( $callback, $callback2 );
2427
2428 $this->preauthMocks = [ $mocks['pre'] ];
2429 $this->primaryauthMocks = [ $mocks['primary'] ];
2430 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2431 $this->initializeManager( true );
2432 $session = $this->request->getSession();
2433
2434 $logger = new \TestLogger( true, function ( $m ) {
2435 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2436 return $m;
2437 } );
2438 $this->manager->setLogger( $logger );
2439
2440 try {
2441 $user = \User::newFromName( 'UTSysop' );
2442 $this->manager->autoCreateUser( $user, 'InvalidSource', true );
2443 $this->fail( 'Expected exception not thrown' );
2444 } catch ( \InvalidArgumentException $ex ) {
2445 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2446 }
2447
2448 // First, check an existing user
2449 $session->clear();
2450 $user = \User::newFromName( 'UTSysop' );
2451 $this->hook( 'LocalUserCreated', $this->never() );
2452 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2453 $this->unhook( 'LocalUserCreated' );
2454 $expect = \Status::newGood();
2455 $expect->warning( 'userexists' );
2456 $this->assertEquals( $expect, $ret );
2457 $this->assertNotEquals( 0, $user->getId() );
2458 $this->assertSame( 'UTSysop', $user->getName() );
2459 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2460 $this->assertSame( [
2461 [ LogLevel::DEBUG, '{username} already exists locally' ],
2462 ], $logger->getBuffer() );
2463 $logger->clearBuffer();
2464
2465 $session->clear();
2466 $user = \User::newFromName( 'UTSysop' );
2467 $this->hook( 'LocalUserCreated', $this->never() );
2468 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2469 $this->unhook( 'LocalUserCreated' );
2470 $expect = \Status::newGood();
2471 $expect->warning( 'userexists' );
2472 $this->assertEquals( $expect, $ret );
2473 $this->assertNotEquals( 0, $user->getId() );
2474 $this->assertSame( 'UTSysop', $user->getName() );
2475 $this->assertEquals( 0, $session->getUser()->getId() );
2476 $this->assertSame( [
2477 [ LogLevel::DEBUG, '{username} already exists locally' ],
2478 ], $logger->getBuffer() );
2479 $logger->clearBuffer();
2480
2481 // Wiki is read-only
2482 $session->clear();
2483 $readOnlyMode = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
2484 $readOnlyMode->setReason( 'Because' );
2485 $user = \User::newFromName( $username );
2486 $this->hook( 'LocalUserCreated', $this->never() );
2487 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2488 $this->unhook( 'LocalUserCreated' );
2489 $this->assertEquals( \Status::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2490 $this->assertEquals( 0, $user->getId() );
2491 $this->assertNotEquals( $username, $user->getName() );
2492 $this->assertEquals( 0, $session->getUser()->getId() );
2493 $this->assertSame( [
2494 [ LogLevel::DEBUG, 'denied by wfReadOnly(): {reason}' ],
2495 ], $logger->getBuffer() );
2496 $logger->clearBuffer();
2497 $readOnlyMode->setReason( false );
2498
2499 // Session blacklisted
2500 $session->clear();
2501 $session->set( 'AuthManager::AutoCreateBlacklist', 'test' );
2502 $user = \User::newFromName( $username );
2503 $this->hook( 'LocalUserCreated', $this->never() );
2504 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2505 $this->unhook( 'LocalUserCreated' );
2506 $this->assertEquals( \Status::newFatal( 'test' ), $ret );
2507 $this->assertEquals( 0, $user->getId() );
2508 $this->assertNotEquals( $username, $user->getName() );
2509 $this->assertEquals( 0, $session->getUser()->getId() );
2510 $this->assertSame( [
2511 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2512 ], $logger->getBuffer() );
2513 $logger->clearBuffer();
2514
2515 $session->clear();
2516 $session->set( 'AuthManager::AutoCreateBlacklist', StatusValue::newFatal( 'test2' ) );
2517 $user = \User::newFromName( $username );
2518 $this->hook( 'LocalUserCreated', $this->never() );
2519 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2520 $this->unhook( 'LocalUserCreated' );
2521 $this->assertEquals( \Status::newFatal( 'test2' ), $ret );
2522 $this->assertEquals( 0, $user->getId() );
2523 $this->assertNotEquals( $username, $user->getName() );
2524 $this->assertEquals( 0, $session->getUser()->getId() );
2525 $this->assertSame( [
2526 [ LogLevel::DEBUG, 'blacklisted in session {sessionid}' ],
2527 ], $logger->getBuffer() );
2528 $logger->clearBuffer();
2529
2530 // Uncreatable name
2531 $session->clear();
2532 $user = \User::newFromName( $username . '@' );
2533 $this->hook( 'LocalUserCreated', $this->never() );
2534 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2535 $this->unhook( 'LocalUserCreated' );
2536 $this->assertEquals( \Status::newFatal( 'noname' ), $ret );
2537 $this->assertEquals( 0, $user->getId() );
2538 $this->assertNotEquals( $username . '@', $user->getId() );
2539 $this->assertEquals( 0, $session->getUser()->getId() );
2540 $this->assertSame( [
2541 [ LogLevel::DEBUG, 'name "{username}" is not creatable' ],
2542 ], $logger->getBuffer() );
2543 $logger->clearBuffer();
2544 $this->assertSame( 'noname', $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2545
2546 // IP unable to create accounts
2547 $this->setGroupPermissions( '*', 'createaccount', false );
2548 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2549 $session->clear();
2550 $user = \User::newFromName( $username );
2551 $this->hook( 'LocalUserCreated', $this->never() );
2552 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2553 $this->unhook( 'LocalUserCreated' );
2554 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-noperm' ), $ret );
2555 $this->assertEquals( 0, $user->getId() );
2556 $this->assertNotEquals( $username, $user->getName() );
2557 $this->assertEquals( 0, $session->getUser()->getId() );
2558 $this->assertSame( [
2559 [ LogLevel::DEBUG, 'IP lacks the ability to create or autocreate accounts' ],
2560 ], $logger->getBuffer() );
2561 $logger->clearBuffer();
2562 $this->assertSame(
2563 'authmanager-autocreate-noperm', $session->get( 'AuthManager::AutoCreateBlacklist' )
2564 );
2565
2566 // maintenance scripts always work
2567 $expectedSource = AuthManager::AUTOCREATE_SOURCE_MAINT;
2568 $this->setGroupPermissions( '*', 'createaccount', false );
2569 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2570 $session->clear();
2571 $user = \User::newFromName( $username );
2572 $this->hook( 'LocalUserCreated', $this->never() );
2573 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_MAINT, true );
2574 $this->unhook( 'LocalUserCreated' );
2575 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2576
2577 // Test that both permutations of permissions are allowed
2578 // (this hits the two "ok" entries in $mocks['pre'])
2579 $expectedSource = AuthManager::AUTOCREATE_SOURCE_SESSION;
2580 $this->setGroupPermissions( '*', 'createaccount', false );
2581 $this->setGroupPermissions( '*', 'autocreateaccount', true );
2582 $session->clear();
2583 $user = \User::newFromName( $username );
2584 $this->hook( 'LocalUserCreated', $this->never() );
2585 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2586 $this->unhook( 'LocalUserCreated' );
2587 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2588
2589 $this->setGroupPermissions( '*', 'createaccount', true );
2590 $this->setGroupPermissions( '*', 'autocreateaccount', false );
2591 $session->clear();
2592 $user = \User::newFromName( $username );
2593 $this->hook( 'LocalUserCreated', $this->never() );
2594 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2595 $this->unhook( 'LocalUserCreated' );
2596 $this->assertEquals( \Status::newFatal( 'ok' ), $ret );
2597 $logger->clearBuffer();
2598
2599 // Test lock fail
2600 $session->clear();
2601 $user = \User::newFromName( $username );
2602 $this->hook( 'LocalUserCreated', $this->never() );
2603 $cache = \ObjectCache::getLocalClusterInstance();
2604 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2605 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2606 unset( $lock );
2607 $this->unhook( 'LocalUserCreated' );
2608 $this->assertEquals( \Status::newFatal( 'usernameinprogress' ), $ret );
2609 $this->assertEquals( 0, $user->getId() );
2610 $this->assertNotEquals( $username, $user->getName() );
2611 $this->assertEquals( 0, $session->getUser()->getId() );
2612 $this->assertSame( [
2613 [ LogLevel::DEBUG, 'Could not acquire account creation lock' ],
2614 ], $logger->getBuffer() );
2615 $logger->clearBuffer();
2616
2617 // Test pre-authentication provider fail
2618 $session->clear();
2619 $user = \User::newFromName( $username );
2620 $this->hook( 'LocalUserCreated', $this->never() );
2621 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2622 $this->unhook( 'LocalUserCreated' );
2623 $this->assertEquals( \Status::newFatal( 'fail-in-pre' ), $ret );
2624 $this->assertEquals( 0, $user->getId() );
2625 $this->assertNotEquals( $username, $user->getName() );
2626 $this->assertEquals( 0, $session->getUser()->getId() );
2627 $this->assertSame( [
2628 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2629 ], $logger->getBuffer() );
2630 $logger->clearBuffer();
2631 $this->assertEquals(
2632 StatusValue::newFatal( 'fail-in-pre' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2633 );
2634
2635 $session->clear();
2636 $user = \User::newFromName( $username );
2637 $this->hook( 'LocalUserCreated', $this->never() );
2638 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2639 $this->unhook( 'LocalUserCreated' );
2640 $this->assertEquals( \Status::newFatal( 'fail-in-primary' ), $ret );
2641 $this->assertEquals( 0, $user->getId() );
2642 $this->assertNotEquals( $username, $user->getName() );
2643 $this->assertEquals( 0, $session->getUser()->getId() );
2644 $this->assertSame( [
2645 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2646 ], $logger->getBuffer() );
2647 $logger->clearBuffer();
2648 $this->assertEquals(
2649 StatusValue::newFatal( 'fail-in-primary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2650 );
2651
2652 $session->clear();
2653 $user = \User::newFromName( $username );
2654 $this->hook( 'LocalUserCreated', $this->never() );
2655 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2656 $this->unhook( 'LocalUserCreated' );
2657 $this->assertEquals( \Status::newFatal( 'fail-in-secondary' ), $ret );
2658 $this->assertEquals( 0, $user->getId() );
2659 $this->assertNotEquals( $username, $user->getName() );
2660 $this->assertEquals( 0, $session->getUser()->getId() );
2661 $this->assertSame( [
2662 [ LogLevel::DEBUG, 'Provider denied creation of {username}: {reason}' ],
2663 ], $logger->getBuffer() );
2664 $logger->clearBuffer();
2665 $this->assertEquals(
2666 StatusValue::newFatal( 'fail-in-secondary' ), $session->get( 'AuthManager::AutoCreateBlacklist' )
2667 );
2668
2669 // Test backoff
2670 $cache = \ObjectCache::getLocalClusterInstance();
2671 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2672 $cache->set( $backoffKey, true );
2673 $session->clear();
2674 $user = \User::newFromName( $username );
2675 $this->hook( 'LocalUserCreated', $this->never() );
2676 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2677 $this->unhook( 'LocalUserCreated' );
2678 $this->assertEquals( \Status::newFatal( 'authmanager-autocreate-exception' ), $ret );
2679 $this->assertEquals( 0, $user->getId() );
2680 $this->assertNotEquals( $username, $user->getName() );
2681 $this->assertEquals( 0, $session->getUser()->getId() );
2682 $this->assertSame( [
2683 [ LogLevel::DEBUG, '{username} denied by prior creation attempt failures' ],
2684 ], $logger->getBuffer() );
2685 $logger->clearBuffer();
2686 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2687 $cache->delete( $backoffKey );
2688
2689 // Test addToDatabase fails
2690 $session->clear();
2691 $user = $this->getMockBuilder( \User::class )
2692 ->setMethods( [ 'addToDatabase' ] )->getMock();
2693 $user->expects( $this->once() )->method( 'addToDatabase' )
2694 ->will( $this->returnValue( \Status::newFatal( 'because' ) ) );
2695 $user->setName( $username );
2696 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2697 $this->assertEquals( \Status::newFatal( 'because' ), $ret );
2698 $this->assertEquals( 0, $user->getId() );
2699 $this->assertNotEquals( $username, $user->getName() );
2700 $this->assertEquals( 0, $session->getUser()->getId() );
2701 $this->assertSame( [
2702 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2703 [ LogLevel::ERROR, '{username} failed with message {msg}' ],
2704 ], $logger->getBuffer() );
2705 $logger->clearBuffer();
2706 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2707
2708 // Test addToDatabase throws an exception
2709 $cache = \ObjectCache::getLocalClusterInstance();
2710 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
2711 $this->assertFalse( $cache->get( $backoffKey ), 'sanity check' );
2712 $session->clear();
2713 $user = $this->getMockBuilder( \User::class )
2714 ->setMethods( [ 'addToDatabase' ] )->getMock();
2715 $user->expects( $this->once() )->method( 'addToDatabase' )
2716 ->will( $this->throwException( new \Exception( 'Excepted' ) ) );
2717 $user->setName( $username );
2718 try {
2719 $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2720 $this->fail( 'Expected exception not thrown' );
2721 } catch ( \Exception $ex ) {
2722 $this->assertSame( 'Excepted', $ex->getMessage() );
2723 }
2724 $this->assertEquals( 0, $user->getId() );
2725 $this->assertEquals( 0, $session->getUser()->getId() );
2726 $this->assertSame( [
2727 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2728 [ LogLevel::ERROR, '{username} failed with exception {exception}' ],
2729 ], $logger->getBuffer() );
2730 $logger->clearBuffer();
2731 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2732 $this->assertNotEquals( false, $cache->get( $backoffKey ) );
2733 $cache->delete( $backoffKey );
2734
2735 // Test addToDatabase fails because the user already exists.
2736 $session->clear();
2737 $user = $this->getMockBuilder( \User::class )
2738 ->setMethods( [ 'addToDatabase' ] )->getMock();
2739 $user->expects( $this->once() )->method( 'addToDatabase' )
2740 ->will( $this->returnCallback( function () use ( $username, &$user ) {
2741 $oldUser = \User::newFromName( $username );
2742 $status = $oldUser->addToDatabase();
2743 $this->assertTrue( $status->isOK(), 'sanity check' );
2744 $user->setId( $oldUser->getId() );
2745 return \Status::newFatal( 'userexists' );
2746 } ) );
2747 $user->setName( $username );
2748 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2749 $expect = \Status::newGood();
2750 $expect->warning( 'userexists' );
2751 $this->assertEquals( $expect, $ret );
2752 $this->assertNotEquals( 0, $user->getId() );
2753 $this->assertEquals( $username, $user->getName() );
2754 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2755 $this->assertSame( [
2756 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2757 [ LogLevel::INFO, '{username} already exists locally (race)' ],
2758 ], $logger->getBuffer() );
2759 $logger->clearBuffer();
2760 $this->assertSame( null, $session->get( 'AuthManager::AutoCreateBlacklist' ) );
2761
2762 // Success!
2763 $session->clear();
2764 $username = self::usernameForCreation();
2765 $user = \User::newFromName( $username );
2766 $this->hook( 'LocalUserCreated', $this->once() )
2767 ->with( $callback, $this->equalTo( true ) );
2768 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, true );
2769 $this->unhook( 'LocalUserCreated' );
2770 $this->assertEquals( \Status::newGood(), $ret );
2771 $this->assertNotEquals( 0, $user->getId() );
2772 $this->assertEquals( $username, $user->getName() );
2773 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
2774 $this->assertSame( [
2775 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2776 ], $logger->getBuffer() );
2777 $logger->clearBuffer();
2778
2779 $dbw = wfGetDB( DB_MASTER );
2780 $maxLogId = $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] );
2781 $session->clear();
2782 $username = self::usernameForCreation();
2783 $user = \User::newFromName( $username );
2784 $this->hook( 'LocalUserCreated', $this->once() )
2785 ->with( $callback, $this->equalTo( true ) );
2786 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2787 $this->unhook( 'LocalUserCreated' );
2788 $this->assertEquals( \Status::newGood(), $ret );
2789 $this->assertNotEquals( 0, $user->getId() );
2790 $this->assertEquals( $username, $user->getName() );
2791 $this->assertEquals( 0, $session->getUser()->getId() );
2792 $this->assertSame( [
2793 [ LogLevel::INFO, 'creating new user ({username}) - from: {from}' ],
2794 ], $logger->getBuffer() );
2795 $logger->clearBuffer();
2796 $this->assertSame(
2797 $maxLogId,
2798 $dbw->selectField( 'logging', 'MAX(log_id)', [ 'log_type' => 'newusers' ] )
2799 );
2800
2801 $this->config->set( 'NewUserLog', true );
2802 $session->clear();
2803 $username = self::usernameForCreation();
2804 $user = \User::newFromName( $username );
2805 $ret = $this->manager->autoCreateUser( $user, AuthManager::AUTOCREATE_SOURCE_SESSION, false );
2806 $this->assertEquals( \Status::newGood(), $ret );
2807 $logger->clearBuffer();
2808
2809 $data = \DatabaseLogEntry::getSelectQueryData();
2810 $rows = iterator_to_array( $dbw->select(
2811 $data['tables'],
2812 $data['fields'],
2813 [
2814 'log_id > ' . (int)$maxLogId,
2815 'log_type' => 'newusers'
2816 ] + $data['conds'],
2817 __METHOD__,
2818 $data['options'],
2819 $data['join_conds']
2820 ) );
2821 $this->assertCount( 1, $rows );
2822 $entry = \DatabaseLogEntry::newFromRow( reset( $rows ) );
2823
2824 $this->assertSame( 'autocreate', $entry->getSubtype() );
2825 $this->assertSame( $user->getId(), $entry->getPerformer()->getId() );
2826 $this->assertSame( $user->getName(), $entry->getPerformer()->getName() );
2827 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2828 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2829
2830 $workaroundPHPUnitBug = true;
2831 }
2832
2833 /**
2834 * @dataProvider provideGetAuthenticationRequests
2835 * @param string $action
2836 * @param array $expect
2837 * @param array $state
2838 */
2839 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
2840 $makeReq = function ( $key ) use ( $action ) {
2841 $req = $this->createMock( AuthenticationRequest::class );
2842 $req->expects( $this->any() )->method( 'getUniqueId' )
2843 ->will( $this->returnValue( $key ) );
2844 $req->action = $action === AuthManager::ACTION_UNLINK ? AuthManager::ACTION_REMOVE : $action;
2845 $req->key = $key;
2846 return $req;
2847 };
2848 $cmpReqs = function ( $a, $b ) {
2849 $ret = strcmp( get_class( $a ), get_class( $b ) );
2850 if ( !$ret ) {
2851 $ret = strcmp( $a->key, $b->key );
2852 }
2853 return $ret;
2854 };
2855
2856 $good = StatusValue::newGood();
2857
2858 $mocks = [];
2859 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2860 $class = ucfirst( $key ) . 'AuthenticationProvider';
2861 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2862 ->setMethods( [
2863 'getUniqueId', 'getAuthenticationRequests', 'providerAllowsAuthenticationDataChange',
2864 ] )
2865 ->getMockForAbstractClass();
2866 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
2867 ->will( $this->returnValue( $key ) );
2868 $mocks[$key]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2869 ->will( $this->returnCallback( function ( $action ) use ( $key, $makeReq ) {
2870 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
2871 } ) );
2872 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsAuthenticationDataChange' )
2873 ->will( $this->returnValue( $good ) );
2874 }
2875
2876 $primaries = [];
2877 foreach ( [
2878 PrimaryAuthenticationProvider::TYPE_NONE,
2879 PrimaryAuthenticationProvider::TYPE_CREATE,
2880 PrimaryAuthenticationProvider::TYPE_LINK
2881 ] as $type ) {
2882 $class = 'PrimaryAuthenticationProvider';
2883 $mocks["primary-$type"] = $this->getMockBuilder( "MediaWiki\\Auth\\$class" )
2884 ->setMethods( [
2885 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2886 'providerAllowsAuthenticationDataChange',
2887 ] )
2888 ->getMockForAbstractClass();
2889 $mocks["primary-$type"]->expects( $this->any() )->method( 'getUniqueId' )
2890 ->will( $this->returnValue( "primary-$type" ) );
2891 $mocks["primary-$type"]->expects( $this->any() )->method( 'accountCreationType' )
2892 ->will( $this->returnValue( $type ) );
2893 $mocks["primary-$type"]->expects( $this->any() )->method( 'getAuthenticationRequests' )
2894 ->will( $this->returnCallback( function ( $action ) use ( $type, $makeReq ) {
2895 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
2896 } ) );
2897 $mocks["primary-$type"]->expects( $this->any() )
2898 ->method( 'providerAllowsAuthenticationDataChange' )
2899 ->will( $this->returnValue( $good ) );
2900 $this->primaryauthMocks[] = $mocks["primary-$type"];
2901 }
2902
2903 $mocks['primary2'] = $this->getMockBuilder( PrimaryAuthenticationProvider::class )
2904 ->setMethods( [
2905 'getUniqueId', 'accountCreationType', 'getAuthenticationRequests',
2906 'providerAllowsAuthenticationDataChange',
2907 ] )
2908 ->getMockForAbstractClass();
2909 $mocks['primary2']->expects( $this->any() )->method( 'getUniqueId' )
2910 ->will( $this->returnValue( 'primary2' ) );
2911 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
2912 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
2913 $mocks['primary2']->expects( $this->any() )->method( 'getAuthenticationRequests' )
2914 ->will( $this->returnValue( [] ) );
2915 $mocks['primary2']->expects( $this->any() )
2916 ->method( 'providerAllowsAuthenticationDataChange' )
2917 ->will( $this->returnCallback( function ( $req ) use ( $good ) {
2918 return $req->key === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
2919 } ) );
2920 $this->primaryauthMocks[] = $mocks['primary2'];
2921
2922 $this->preauthMocks = [ $mocks['pre'] ];
2923 $this->secondaryauthMocks = [ $mocks['secondary'] ];
2924 $this->initializeManager( true );
2925
2926 if ( $state ) {
2927 if ( isset( $state['continueRequests'] ) ) {
2928 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
2929 }
2930 if ( $action === AuthManager::ACTION_LOGIN_CONTINUE ) {
2931 $this->request->getSession()->setSecret( 'AuthManager::authnState', $state );
2932 } elseif ( $action === AuthManager::ACTION_CREATE_CONTINUE ) {
2933 $this->request->getSession()->setSecret( 'AuthManager::accountCreationState', $state );
2934 } elseif ( $action === AuthManager::ACTION_LINK_CONTINUE ) {
2935 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', $state );
2936 }
2937 }
2938
2939 $expectReqs = array_map( $makeReq, $expect );
2940 if ( $action === AuthManager::ACTION_LOGIN ) {
2941 $req = new RememberMeAuthenticationRequest;
2942 $req->action = $action;
2943 $req->required = AuthenticationRequest::REQUIRED;
2944 $expectReqs[] = $req;
2945 } elseif ( $action === AuthManager::ACTION_CREATE ) {
2946 $req = new UsernameAuthenticationRequest;
2947 $req->action = $action;
2948 $expectReqs[] = $req;
2949 $req = new UserDataAuthenticationRequest;
2950 $req->action = $action;
2951 $req->required = AuthenticationRequest::REQUIRED;
2952 $expectReqs[] = $req;
2953 }
2954 usort( $expectReqs, $cmpReqs );
2955
2956 $actual = $this->manager->getAuthenticationRequests( $action );
2957 foreach ( $actual as $req ) {
2958 // Don't test this here.
2959 $req->required = AuthenticationRequest::REQUIRED;
2960 }
2961 usort( $actual, $cmpReqs );
2962
2963 $this->assertEquals( $expectReqs, $actual );
2964
2965 // Test CreationReasonAuthenticationRequest gets returned
2966 if ( $action === AuthManager::ACTION_CREATE ) {
2967 $req = new CreationReasonAuthenticationRequest;
2968 $req->action = $action;
2969 $req->required = AuthenticationRequest::REQUIRED;
2970 $expectReqs[] = $req;
2971 usort( $expectReqs, $cmpReqs );
2972
2973 $actual = $this->manager->getAuthenticationRequests( $action, \User::newFromName( 'UTSysop' ) );
2974 foreach ( $actual as $req ) {
2975 // Don't test this here.
2976 $req->required = AuthenticationRequest::REQUIRED;
2977 }
2978 usort( $actual, $cmpReqs );
2979
2980 $this->assertEquals( $expectReqs, $actual );
2981 }
2982 }
2983
2984 public static function provideGetAuthenticationRequests() {
2985 return [
2986 [
2987 AuthManager::ACTION_LOGIN,
2988 [ 'pre-login', 'primary-none-login', 'primary-create-login',
2989 'primary-link-login', 'secondary-login', 'generic' ],
2990 ],
2991 [
2992 AuthManager::ACTION_CREATE,
2993 [ 'pre-create', 'primary-none-create', 'primary-create-create',
2994 'primary-link-create', 'secondary-create', 'generic' ],
2995 ],
2996 [
2997 AuthManager::ACTION_LINK,
2998 [ 'primary-link-link', 'generic' ],
2999 ],
3000 [
3001 AuthManager::ACTION_CHANGE,
3002 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3003 'secondary-change' ],
3004 ],
3005 [
3006 AuthManager::ACTION_REMOVE,
3007 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3008 'secondary-remove' ],
3009 ],
3010 [
3011 AuthManager::ACTION_UNLINK,
3012 [ 'primary-link-remove' ],
3013 ],
3014 [
3015 AuthManager::ACTION_LOGIN_CONTINUE,
3016 [],
3017 ],
3018 [
3019 AuthManager::ACTION_LOGIN_CONTINUE,
3020 $reqs = [ 'continue-login', 'foo', 'bar' ],
3021 [
3022 'continueRequests' => $reqs,
3023 ],
3024 ],
3025 [
3026 AuthManager::ACTION_CREATE_CONTINUE,
3027 [],
3028 ],
3029 [
3030 AuthManager::ACTION_CREATE_CONTINUE,
3031 $reqs = [ 'continue-create', 'foo', 'bar' ],
3032 [
3033 'continueRequests' => $reqs,
3034 ],
3035 ],
3036 [
3037 AuthManager::ACTION_LINK_CONTINUE,
3038 [],
3039 ],
3040 [
3041 AuthManager::ACTION_LINK_CONTINUE,
3042 $reqs = [ 'continue-link', 'foo', 'bar' ],
3043 [
3044 'continueRequests' => $reqs,
3045 ],
3046 ],
3047 ];
3048 }
3049
3050 public function testGetAuthenticationRequestsRequired() {
3051 $makeReq = function ( $key, $required ) {
3052 $req = $this->createMock( AuthenticationRequest::class );
3053 $req->expects( $this->any() )->method( 'getUniqueId' )
3054 ->will( $this->returnValue( $key ) );
3055 $req->action = AuthManager::ACTION_LOGIN;
3056 $req->key = $key;
3057 $req->required = $required;
3058 return $req;
3059 };
3060 $cmpReqs = function ( $a, $b ) {
3061 $ret = strcmp( get_class( $a ), get_class( $b ) );
3062 if ( !$ret ) {
3063 $ret = strcmp( $a->key, $b->key );
3064 }
3065 return $ret;
3066 };
3067
3068 $good = StatusValue::newGood();
3069
3070 $primary1 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3071 $primary1->expects( $this->any() )->method( 'getUniqueId' )
3072 ->will( $this->returnValue( 'primary1' ) );
3073 $primary1->expects( $this->any() )->method( 'accountCreationType' )
3074 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3075 $primary1->expects( $this->any() )->method( 'getAuthenticationRequests' )
3076 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3077 return [
3078 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3079 $makeReq( "required", AuthenticationRequest::REQUIRED ),
3080 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3081 $makeReq( "foo", AuthenticationRequest::REQUIRED ),
3082 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3083 $makeReq( "baz", AuthenticationRequest::OPTIONAL ),
3084 ];
3085 } ) );
3086
3087 $primary2 = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3088 $primary2->expects( $this->any() )->method( 'getUniqueId' )
3089 ->will( $this->returnValue( 'primary2' ) );
3090 $primary2->expects( $this->any() )->method( 'accountCreationType' )
3091 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3092 $primary2->expects( $this->any() )->method( 'getAuthenticationRequests' )
3093 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3094 return [
3095 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3096 $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3097 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3098 ];
3099 } ) );
3100
3101 $secondary = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3102 $secondary->expects( $this->any() )->method( 'getUniqueId' )
3103 ->will( $this->returnValue( 'secondary' ) );
3104 $secondary->expects( $this->any() )->method( 'getAuthenticationRequests' )
3105 ->will( $this->returnCallback( function ( $action ) use ( $makeReq ) {
3106 return [
3107 $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3108 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3109 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3110 ];
3111 } ) );
3112
3113 $rememberReq = new RememberMeAuthenticationRequest;
3114 $rememberReq->action = AuthManager::ACTION_LOGIN;
3115
3116 $this->primaryauthMocks = [ $primary1, $primary2 ];
3117 $this->secondaryauthMocks = [ $secondary ];
3118 $this->initializeManager( true );
3119
3120 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3121 $expected = [
3122 $rememberReq,
3123 $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
3124 $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3125 $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
3126 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3127 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3128 $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3129 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3130 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3131 ];
3132 usort( $actual, $cmpReqs );
3133 usort( $expected, $cmpReqs );
3134 $this->assertEquals( $expected, $actual );
3135
3136 $this->primaryauthMocks = [ $primary1 ];
3137 $this->secondaryauthMocks = [ $secondary ];
3138 $this->initializeManager( true );
3139
3140 $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
3141 $expected = [
3142 $rememberReq,
3143 $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
3144 $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
3145 $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
3146 $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
3147 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3148 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3149 ];
3150 usort( $actual, $cmpReqs );
3151 usort( $expected, $cmpReqs );
3152 $this->assertEquals( $expected, $actual );
3153 }
3154
3155 public function testAllowsPropertyChange() {
3156 $mocks = [];
3157 foreach ( [ 'primary', 'secondary' ] as $key ) {
3158 $class = ucfirst( $key ) . 'AuthenticationProvider';
3159 $mocks[$key] = $this->getMockForAbstractClass( "MediaWiki\\Auth\\$class" );
3160 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3161 ->will( $this->returnValue( $key ) );
3162 $mocks[$key]->expects( $this->any() )->method( 'providerAllowsPropertyChange' )
3163 ->will( $this->returnCallback( function ( $prop ) use ( $key ) {
3164 return $prop !== $key;
3165 } ) );
3166 }
3167
3168 $this->primaryauthMocks = [ $mocks['primary'] ];
3169 $this->secondaryauthMocks = [ $mocks['secondary'] ];
3170 $this->initializeManager( true );
3171
3172 $this->assertTrue( $this->manager->allowsPropertyChange( 'foo' ) );
3173 $this->assertFalse( $this->manager->allowsPropertyChange( 'primary' ) );
3174 $this->assertFalse( $this->manager->allowsPropertyChange( 'secondary' ) );
3175 }
3176
3177 public function testAutoCreateOnLogin() {
3178 $username = self::usernameForCreation();
3179
3180 $req = $this->createMock( AuthenticationRequest::class );
3181
3182 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3183 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3184 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3185 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3186 $mock->expects( $this->any() )->method( 'accountCreationType' )
3187 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3188 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3189 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3190 ->will( $this->returnValue( StatusValue::newGood() ) );
3191
3192 $mock2 = $this->getMockForAbstractClass( SecondaryAuthenticationProvider::class );
3193 $mock2->expects( $this->any() )->method( 'getUniqueId' )
3194 ->will( $this->returnValue( 'secondary' ) );
3195 $mock2->expects( $this->any() )->method( 'beginSecondaryAuthentication' )->will(
3196 $this->returnValue(
3197 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) )
3198 )
3199 );
3200 $mock2->expects( $this->any() )->method( 'continueSecondaryAuthentication' )
3201 ->will( $this->returnValue( AuthenticationResponse::newAbstain() ) );
3202 $mock2->expects( $this->any() )->method( 'testUserForCreation' )
3203 ->will( $this->returnValue( StatusValue::newGood() ) );
3204
3205 $this->primaryauthMocks = [ $mock ];
3206 $this->secondaryauthMocks = [ $mock2 ];
3207 $this->initializeManager( true );
3208 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3209 $session = $this->request->getSession();
3210 $session->clear();
3211
3212 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3213 'sanity check' );
3214
3215 $callback = $this->callback( function ( $user ) use ( $username ) {
3216 return $user->getName() === $username;
3217 } );
3218
3219 $this->hook( 'UserLoggedIn', $this->never() );
3220 $this->hook( 'LocalUserCreated', $this->once() )->with( $callback, $this->equalTo( true ) );
3221 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3222 $this->unhook( 'LocalUserCreated' );
3223 $this->unhook( 'UserLoggedIn' );
3224 $this->assertSame( AuthenticationResponse::UI, $ret->status );
3225
3226 $id = (int)\User::newFromName( $username )->getId();
3227 $this->assertNotSame( 0, \User::newFromName( $username )->getId() );
3228 $this->assertSame( 0, $session->getUser()->getId() );
3229
3230 $this->hook( 'UserLoggedIn', $this->once() )->with( $callback );
3231 $this->hook( 'LocalUserCreated', $this->never() );
3232 $ret = $this->manager->continueAuthentication( [] );
3233 $this->unhook( 'LocalUserCreated' );
3234 $this->unhook( 'UserLoggedIn' );
3235 $this->assertSame( AuthenticationResponse::PASS, $ret->status );
3236 $this->assertSame( $username, $ret->username );
3237 $this->assertSame( $id, $session->getUser()->getId() );
3238 }
3239
3240 public function testAutoCreateFailOnLogin() {
3241 $username = self::usernameForCreation();
3242
3243 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3244 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'primary' ) );
3245 $mock->expects( $this->any() )->method( 'beginPrimaryAuthentication' )
3246 ->will( $this->returnValue( AuthenticationResponse::newPass( $username ) ) );
3247 $mock->expects( $this->any() )->method( 'accountCreationType' )
3248 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3249 $mock->expects( $this->any() )->method( 'testUserExists' )->will( $this->returnValue( true ) );
3250 $mock->expects( $this->any() )->method( 'testUserForCreation' )
3251 ->will( $this->returnValue( StatusValue::newFatal( 'fail-from-primary' ) ) );
3252
3253 $this->primaryauthMocks = [ $mock ];
3254 $this->initializeManager( true );
3255 $this->manager->setLogger( new \Psr\Log\NullLogger() );
3256 $session = $this->request->getSession();
3257 $session->clear();
3258
3259 $this->assertSame( 0, $session->getUser()->getId(),
3260 'sanity check' );
3261 $this->assertSame( 0, \User::newFromName( $username )->getId(),
3262 'sanity check' );
3263
3264 $this->hook( 'UserLoggedIn', $this->never() );
3265 $this->hook( 'LocalUserCreated', $this->never() );
3266 $ret = $this->manager->beginAuthentication( [], 'http://localhost/' );
3267 $this->unhook( 'LocalUserCreated' );
3268 $this->unhook( 'UserLoggedIn' );
3269 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3270 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message->getKey() );
3271
3272 $this->assertSame( 0, \User::newFromName( $username )->getId() );
3273 $this->assertSame( 0, $session->getUser()->getId() );
3274 }
3275
3276 public function testAuthenticationSessionData() {
3277 $this->initializeManager( true );
3278
3279 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3280 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3281 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3282 $this->assertSame( 'foo!', $this->manager->getAuthenticationSessionData( 'foo' ) );
3283 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3284 $this->manager->removeAuthenticationSessionData( 'foo' );
3285 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3286 $this->assertSame( 'bar!', $this->manager->getAuthenticationSessionData( 'bar' ) );
3287 $this->manager->removeAuthenticationSessionData( 'bar' );
3288 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3289
3290 $this->manager->setAuthenticationSessionData( 'foo', 'foo!' );
3291 $this->manager->setAuthenticationSessionData( 'bar', 'bar!' );
3292 $this->manager->removeAuthenticationSessionData( null );
3293 $this->assertNull( $this->manager->getAuthenticationSessionData( 'foo' ) );
3294 $this->assertNull( $this->manager->getAuthenticationSessionData( 'bar' ) );
3295 }
3296
3297 public function testCanLinkAccounts() {
3298 $types = [
3299 PrimaryAuthenticationProvider::TYPE_CREATE => true,
3300 PrimaryAuthenticationProvider::TYPE_LINK => true,
3301 PrimaryAuthenticationProvider::TYPE_NONE => false,
3302 ];
3303
3304 foreach ( $types as $type => $can ) {
3305 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3306 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( $type ) );
3307 $mock->expects( $this->any() )->method( 'accountCreationType' )
3308 ->will( $this->returnValue( $type ) );
3309 $this->primaryauthMocks = [ $mock ];
3310 $this->initializeManager( true );
3311 $this->assertSame( $can, $this->manager->canCreateAccounts(), $type );
3312 }
3313 }
3314
3315 public function testBeginAccountLink() {
3316 $user = \User::newFromName( 'UTSysop' );
3317 $this->initializeManager();
3318
3319 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', 'test' );
3320 try {
3321 $this->manager->beginAccountLink( $user, [], 'http://localhost/' );
3322 $this->fail( 'Expected exception not thrown' );
3323 } catch ( \LogicException $ex ) {
3324 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3325 }
3326 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3327
3328 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3329 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3330 $mock->expects( $this->any() )->method( 'accountCreationType' )
3331 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3332 $this->primaryauthMocks = [ $mock ];
3333 $this->initializeManager( true );
3334
3335 $ret = $this->manager->beginAccountLink( new \User, [], 'http://localhost/' );
3336 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3337 $this->assertSame( 'noname', $ret->message->getKey() );
3338
3339 $ret = $this->manager->beginAccountLink(
3340 \User::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3341 );
3342 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3343 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message->getKey() );
3344 }
3345
3346 public function testContinueAccountLink() {
3347 $user = \User::newFromName( 'UTSysop' );
3348 $this->initializeManager();
3349
3350 $session = [
3351 'userid' => $user->getId(),
3352 'username' => $user->getName(),
3353 'primary' => 'X',
3354 ];
3355
3356 try {
3357 $this->manager->continueAccountLink( [] );
3358 $this->fail( 'Expected exception not thrown' );
3359 } catch ( \LogicException $ex ) {
3360 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3361 }
3362
3363 $mock = $this->getMockForAbstractClass( PrimaryAuthenticationProvider::class );
3364 $mock->expects( $this->any() )->method( 'getUniqueId' )->will( $this->returnValue( 'X' ) );
3365 $mock->expects( $this->any() )->method( 'accountCreationType' )
3366 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3367 $mock->expects( $this->any() )->method( 'beginPrimaryAccountLink' )->will(
3368 $this->returnValue( AuthenticationResponse::newFail( $this->message( 'fail' ) ) )
3369 );
3370 $this->primaryauthMocks = [ $mock ];
3371 $this->initializeManager( true );
3372
3373 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState', null );
3374 $ret = $this->manager->continueAccountLink( [] );
3375 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3376 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message->getKey() );
3377
3378 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3379 [ 'username' => $user->getName() . '<>' ] + $session );
3380 $ret = $this->manager->continueAccountLink( [] );
3381 $this->assertSame( AuthenticationResponse::FAIL, $ret->status );
3382 $this->assertSame( 'noname', $ret->message->getKey() );
3383 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3384
3385 $id = $user->getId();
3386 $this->request->getSession()->setSecret( 'AuthManager::accountLinkState',
3387 [ 'userid' => $id + 1 ] + $session );
3388 try {
3389 $ret = $this->manager->continueAccountLink( [] );
3390 $this->fail( 'Expected exception not thrown' );
3391 } catch ( \UnexpectedValueException $ex ) {
3392 $this->assertEquals(
3393 "User \"{$user->getName()}\" is valid, but ID $id !== " . ( $id + 1 ) . '!',
3394 $ex->getMessage()
3395 );
3396 }
3397 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ) );
3398 }
3399
3400 /**
3401 * @dataProvider provideAccountLink
3402 * @param StatusValue $preTest
3403 * @param array $primaryResponses
3404 * @param array $managerResponses
3405 */
3406 public function testAccountLink(
3407 StatusValue $preTest, array $primaryResponses, array $managerResponses
3408 ) {
3409 $user = \User::newFromName( 'UTSysop' );
3410
3411 $this->initializeManager();
3412
3413 // Set up lots of mocks...
3414 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3415 $req->primary = $primaryResponses;
3416 $mocks = [];
3417
3418 foreach ( [ 'pre', 'primary' ] as $key ) {
3419 $class = ucfirst( $key ) . 'AuthenticationProvider';
3420 $mocks[$key] = $this->getMockForAbstractClass(
3421 "MediaWiki\\Auth\\$class", [], "Mock$class"
3422 );
3423 $mocks[$key]->expects( $this->any() )->method( 'getUniqueId' )
3424 ->will( $this->returnValue( $key ) );
3425
3426 for ( $i = 2; $i <= 3; $i++ ) {
3427 $mocks[$key . $i] = $this->getMockForAbstractClass(
3428 "MediaWiki\\Auth\\$class", [], "Mock$class"
3429 );
3430 $mocks[$key . $i]->expects( $this->any() )->method( 'getUniqueId' )
3431 ->will( $this->returnValue( $key . $i ) );
3432 }
3433 }
3434
3435 $mocks['pre']->expects( $this->any() )->method( 'testForAccountLink' )
3436 ->will( $this->returnCallback(
3437 function ( $u )
3438 use ( $user, $preTest )
3439 {
3440 $this->assertSame( $user->getId(), $u->getId() );
3441 $this->assertSame( $user->getName(), $u->getName() );
3442 return $preTest;
3443 }
3444 ) );
3445
3446 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3447 ->will( $this->returnValue( StatusValue::newGood() ) );
3448
3449 $mocks['primary']->expects( $this->any() )->method( 'accountCreationType' )
3450 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3451 $ct = count( $req->primary );
3452 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req ) {
3453 $this->assertSame( $user->getId(), $u->getId() );
3454 $this->assertSame( $user->getName(), $u->getName() );
3455 $foundReq = false;
3456 foreach ( $reqs as $r ) {
3457 $this->assertSame( $user->getName(), $r->username );
3458 $foundReq = $foundReq || get_class( $r ) === get_class( $req );
3459 }
3460 $this->assertTrue( $foundReq, '$reqs contains $req' );
3461 return array_shift( $req->primary );
3462 } );
3463 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
3464 ->method( 'beginPrimaryAccountLink' )
3465 ->will( $callback );
3466 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
3467 ->method( 'continuePrimaryAccountLink' )
3468 ->will( $callback );
3469
3470 $abstain = AuthenticationResponse::newAbstain();
3471 $mocks['primary2']->expects( $this->any() )->method( 'accountCreationType' )
3472 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_LINK ) );
3473 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
3474 ->will( $this->returnValue( $abstain ) );
3475 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3476 $mocks['primary3']->expects( $this->any() )->method( 'accountCreationType' )
3477 ->will( $this->returnValue( PrimaryAuthenticationProvider::TYPE_CREATE ) );
3478 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
3479 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
3480
3481 $this->preauthMocks = [ $mocks['pre'], $mocks['pre2'] ];
3482 $this->primaryauthMocks = [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
3483 $this->logger = new \TestLogger( true, function ( $message, $level ) {
3484 return $level === LogLevel::DEBUG ? null : $message;
3485 } );
3486 $this->initializeManager( true );
3487
3488 $constraint = \PHPUnit_Framework_Assert::logicalOr(
3489 $this->equalTo( AuthenticationResponse::PASS ),
3490 $this->equalTo( AuthenticationResponse::FAIL )
3491 );
3492 $providers = array_merge( $this->preauthMocks, $this->primaryauthMocks );
3493 foreach ( $providers as $p ) {
3494 $p->postCalled = false;
3495 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
3496 ->willReturnCallback( function ( $user, $response ) use ( $constraint, $p ) {
3497 $this->assertInstanceOf( \User::class, $user );
3498 $this->assertSame( 'UTSysop', $user->getName() );
3499 $this->assertInstanceOf( AuthenticationResponse::class, $response );
3500 $this->assertThat( $response->status, $constraint );
3501 $p->postCalled = $response->status;
3502 } );
3503 }
3504
3505 $first = true;
3506 $created = false;
3507 $expectLog = [];
3508 foreach ( $managerResponses as $i => $response ) {
3509 if ( $response instanceof AuthenticationResponse &&
3510 $response->status === AuthenticationResponse::PASS
3511 ) {
3512 $expectLog[] = [ LogLevel::INFO, 'Account linked to {user} by primary' ];
3513 }
3514
3515 $ex = null;
3516 try {
3517 if ( $first ) {
3518 $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
3519 } else {
3520 $ret = $this->manager->continueAccountLink( [ $req ] );
3521 }
3522 if ( $response instanceof \Exception ) {
3523 $this->fail( 'Expected exception not thrown', "Response $i" );
3524 }
3525 } catch ( \Exception $ex ) {
3526 if ( !$response instanceof \Exception ) {
3527 throw $ex;
3528 }
3529 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
3530 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3531 "Response $i, exception, session state" );
3532 return;
3533 }
3534
3535 $this->assertSame( 'http://localhost/', $req->returnToUrl );
3536
3537 $ret->message = $this->message( $ret->message );
3538 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
3539 if ( $response->status === AuthenticationResponse::PASS ||
3540 $response->status === AuthenticationResponse::FAIL
3541 ) {
3542 $this->assertNull( $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3543 "Response $i, session state" );
3544 foreach ( $providers as $p ) {
3545 $this->assertSame( $response->status, $p->postCalled,
3546 "Response $i, post-auth callback called" );
3547 }
3548 } else {
3549 $this->assertNotNull(
3550 $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' ),
3551 "Response $i, session state"
3552 );
3553 foreach ( $ret->neededRequests as $neededReq ) {
3554 $this->assertEquals( AuthManager::ACTION_LINK, $neededReq->action,
3555 "Response $i, neededRequest action" );
3556 }
3557 $this->assertEquals(
3558 $ret->neededRequests,
3559 $this->manager->getAuthenticationRequests( AuthManager::ACTION_LINK_CONTINUE ),
3560 "Response $i, continuation check"
3561 );
3562 foreach ( $providers as $p ) {
3563 $this->assertFalse( $p->postCalled, "Response $i, post-auth callback not called" );
3564 }
3565 }
3566
3567 $first = false;
3568 }
3569
3570 $this->assertSame( $expectLog, $this->logger->getBuffer() );
3571 }
3572
3573 public function provideAccountLink() {
3574 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
3575 $good = StatusValue::newGood();
3576
3577 return [
3578 'Pre-link test fail in pre' => [
3579 StatusValue::newFatal( 'fail-from-pre' ),
3580 [],
3581 [
3582 AuthenticationResponse::newFail( $this->message( 'fail-from-pre' ) ),
3583 ]
3584 ],
3585 'Failure in primary' => [
3586 $good,
3587 $tmp = [
3588 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
3589 ],
3590 $tmp
3591 ],
3592 'All primary abstain' => [
3593 $good,
3594 [
3595 AuthenticationResponse::newAbstain(),
3596 ],
3597 [
3598 AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
3599 ]
3600 ],
3601 'Primary UI, then redirect, then fail' => [
3602 $good,
3603 $tmp = [
3604 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3605 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
3606 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
3607 ],
3608 $tmp
3609 ],
3610 'Primary redirect, then abstain' => [
3611 $good,
3612 [
3613 $tmp = AuthenticationResponse::newRedirect(
3614 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
3615 ),
3616 AuthenticationResponse::newAbstain(),
3617 ],
3618 [
3619 $tmp,
3620 new \DomainException(
3621 'MockPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
3622 )
3623 ]
3624 ],
3625 'Primary UI, then pass' => [
3626 $good,
3627 [
3628 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
3629 AuthenticationResponse::newPass(),
3630 ],
3631 [
3632 $tmp1,
3633 AuthenticationResponse::newPass( '' ),
3634 ]
3635 ],
3636 'Primary pass' => [
3637 $good,
3638 [
3639 AuthenticationResponse::newPass( '' ),
3640 ],
3641 [
3642 AuthenticationResponse::newPass( '' ),
3643 ]
3644 ],
3645 ];
3646 }
3647 }