3 namespace MediaWiki\Tests\Auth
;
8 use DummySessionProvider
;
9 use DynamicPropertyTestHelper
;
11 use InvalidArgumentException
;
13 use MediaWiki\Auth\AbstractPreAuthenticationProvider
;
14 use MediaWiki\Auth\AbstractPrimaryAuthenticationProvider
;
15 use MediaWiki\Auth\AbstractSecondaryAuthenticationProvider
;
16 use MediaWiki\Auth\AuthenticationProvider
;
17 use MediaWiki\Auth\AuthenticationRequest
;
18 use MediaWiki\Auth\AuthenticationResponse
;
19 use MediaWiki\Auth\AuthManager
;
20 use MediaWiki\Auth\ConfirmLinkSecondaryAuthenticationProvider
;
21 use MediaWiki\Auth\CreatedAccountAuthenticationRequest
;
22 use MediaWiki\Auth\CreateFromLoginAuthenticationRequest
;
23 use MediaWiki\Auth\CreationReasonAuthenticationRequest
;
24 use MediaWiki\Auth\Hook\AuthManagerLoginAuthenticateAuditHook
;
25 use MediaWiki\Auth\Hook\AuthManagerVerifyAuthenticationHook
;
26 use MediaWiki\Auth\Hook\LocalUserCreatedHook
;
27 use MediaWiki\Auth\Hook\SecuritySensitiveOperationStatusHook
;
28 use MediaWiki\Auth\Hook\UserLoggedInHook
;
29 use MediaWiki\Auth\PasswordAuthenticationRequest
;
30 use MediaWiki\Auth\PrimaryAuthenticationProvider
;
31 use MediaWiki\Auth\RememberMeAuthenticationRequest
;
32 use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest
;
33 use MediaWiki\Auth\UserDataAuthenticationRequest
;
34 use MediaWiki\Auth\UsernameAuthenticationRequest
;
35 use MediaWiki\Block\BlockManager
;
36 use MediaWiki\Block\DatabaseBlock
;
37 use MediaWiki\Block\Restriction\PageRestriction
;
38 use MediaWiki\Block\SystemBlock
;
39 use MediaWiki\Config\Config
;
40 use MediaWiki\Config\HashConfig
;
41 use MediaWiki\Config\ServiceOptions
;
42 use MediaWiki\Context\RequestContext
;
43 use MediaWiki\HookContainer\HookContainer
;
44 use MediaWiki\HookContainer\StaticHookRegistry
;
45 use MediaWiki\Language\Language
;
46 use MediaWiki\Languages\LanguageConverterFactory
;
47 use MediaWiki\Logger\LoggerFactory
;
48 use MediaWiki\MainConfigNames
;
49 use MediaWiki\MediaWikiServices
;
50 use MediaWiki\Message\Message
;
51 use MediaWiki\Request\FauxRequest
;
52 use MediaWiki\Request\WebRequest
;
53 use MediaWiki\Session\SessionInfo
;
54 use MediaWiki\Session\SessionManager
;
55 use MediaWiki\Session\UserInfo
;
56 use MediaWiki\Status\Status
;
57 use MediaWiki\Tests\Session\TestUtils
;
58 use MediaWiki\User\BotPasswordStore
;
59 use MediaWiki\User\Options\UserOptionsManager
;
60 use MediaWiki\User\User
;
61 use MediaWiki\User\UserFactory
;
62 use MediaWiki\User\UserIdentityLookup
;
63 use MediaWiki\User\UserNameUtils
;
64 use MediaWiki\Watchlist\WatchlistManager
;
65 use MediaWikiIntegrationTestCase
;
66 use ObjectCacheFactory
;
67 use PHPUnit\Framework\Assert
;
68 use PHPUnit\Framework\MockObject\Builder\InvocationMocker
;
69 use PHPUnit\Framework\MockObject\MockObject
;
70 use PHPUnit\Framework\MockObject\Rule\InvocationOrder
;
71 use Psr\Container\ContainerInterface
;
72 use Psr\Log\LoggerInterface
;
74 use Psr\Log\NullLogger
;
80 use UnexpectedValueException
;
81 use Wikimedia\Message\MessageSpecifier
;
82 use Wikimedia\ObjectCache\HashBagOStuff
;
83 use Wikimedia\ObjectFactory\ObjectFactory
;
84 use Wikimedia\Rdbms\ILoadBalancer
;
85 use Wikimedia\Rdbms\ReadOnlyMode
;
86 use Wikimedia\ScopedCallback
;
87 use Wikimedia\TestingAccessWrapper
;
92 * @covers \MediaWiki\Auth\AuthManager
94 class AuthManagerTest
extends MediaWikiIntegrationTestCase
{
95 protected WebRequest
$request;
96 protected Config
$config;
97 protected ObjectFactory
$objectFactory;
98 protected ReadOnlyMode
$readOnlyMode;
99 private HookContainer
$hookContainer;
100 protected UserNameUtils
$userNameUtils;
101 protected LoggerInterface
$logger;
103 /** @var AbstractPreAuthenticationProvider&MockObject[] */
104 protected $preauthMocks = [];
105 /** @var AbstractPrimaryAuthenticationProvider&MockObject[] */
106 protected $primaryauthMocks = [];
107 /** @var AbstractSecondaryAuthenticationProvider&MockObject[] */
108 protected $secondaryauthMocks = [];
110 protected AuthManager
$manager;
111 /** @var TestingAccessWrapper|AuthManager */
112 protected $managerPriv;
114 private BlockManager
$blockManager;
115 private WatchlistManager
$watchlistManager;
116 private ILoadBalancer
$loadBalancer;
117 private Language
$contentLanguage;
118 private LanguageConverterFactory
$languageConverterFactory;
119 private BotPasswordStore
$botPasswordStore;
120 private UserFactory
$userFactory;
121 private UserIdentityLookup
$userIdentityLookup;
122 private UserOptionsManager
$userOptionsManager;
123 private ObjectCacheFactory
$objectCacheFactory;
126 * Registers a mock hook.
127 * Note this should be called after initializeManager( true ) as that removes mock hooks.
128 * @param string $hook
129 * @param string $hookInterface
130 * @param InvocationOrder $expect From $this->once(), $this->never(), etc.
131 * @return InvocationMocker $mock->expects( $expect )->method( ... ).
133 protected function hook( $hook, $hookInterface, $expect ) {
134 $mock = $this->getMockBuilder( $hookInterface )
135 ->onlyMethods( [ "on$hook" ] )
137 $this->hookContainer
->register( $hook, $mock );
138 return $mock->expects( $expect )->method( "on$hook" );
143 * @param string $hook
145 protected function unhook( $hook ) {
146 $this->hookContainer
->clear( $hook );
150 * Ensure a value is a clean Message object
152 * @param string|Message $key
153 * @param array $params
157 protected function message( $key, $params = [] ) {
158 if ( $key === null ) {
161 if ( $key instanceof MessageSpecifier
) {
162 $params = $key->getParams();
163 $key = $key->getKey();
165 return new Message( $key, $params,
166 MediaWikiServices
::getInstance()->getLanguageFactory()->getLanguage( 'en' ) );
170 * Test two AuthenticationResponses for equality. We don't want to use regular assertEquals
171 * because that recursively compares members, which leads to false negatives if e.g. Language
174 * @param AuthenticationResponse $expected
175 * @param AuthenticationResponse $actual
178 private function assertResponseEquals(
179 AuthenticationResponse
$expected, AuthenticationResponse
$actual, $msg = ''
181 foreach ( ( new ReflectionClass( $expected ) )->getProperties() as $prop ) {
182 $name = $prop->getName();
183 $usedMsg = ltrim( "$msg ($name)" );
184 if ( $name === 'message' && $expected->message
) {
185 $this->assertSame( $expected->message
->__serialize(), $actual->message
->__serialize(),
188 $this->assertEquals( $expected->$name, $actual->$name, $usedMsg );
194 * Initialize the AuthManagerConfig variable in $this->config
196 * Uses data from the various 'mocks' fields.
198 protected function initializeConfig() {
208 foreach ( [ 'preauth', 'primaryauth', 'secondaryauth' ] as $type ) {
209 $key = $type . 'Mocks';
210 foreach ( $this->$key as $mock ) {
211 $config[$type][$mock->getUniqueId()] = [ 'factory' => static function () use ( $mock ) {
217 $this->config
->set( MainConfigNames
::AuthManagerConfig
, $config );
218 $this->config
->set( MainConfigNames
::LanguageCode
, 'en' );
219 $this->config
->set( MainConfigNames
::NewUserLog
, false );
220 $this->config
->set( MainConfigNames
::RememberMe
, RememberMeAuthenticationRequest
::CHOOSE_REMEMBER
);
224 * Initialize $this->manager
225 * @param bool $regen Force a call to $this->initializeConfig()
227 protected function initializeManager( $regen = false ) {
228 if ( $regen ||
!isset( $this->config
) ) {
229 $this->config
= new HashConfig();
231 if ( $regen ||
!isset( $this->request
) ) {
232 $this->request
= new FauxRequest();
235 $this->objectFactory ??
= new ObjectFactory( $this->createNoOpAbstractMock( ContainerInterface
::class ) );
236 $this->readOnlyMode ??
= $this->getServiceContainer()->getReadOnlyMode();
237 // Override BlockManager::checkHost. Formerly testAuthorizeCreateAccount_DNSBlacklist
238 // required *.localhost to resolve as 127.0.0.1, but that is system-dependent.
239 $this->blockManager ??
= new class(
241 BlockManager
::CONSTRUCTOR_OPTIONS
,
242 $this->getServiceContainer()->getMainConfig()
244 $this->getServiceContainer()->getUserFactory(),
245 $this->getServiceContainer()->getUserIdentityUtils(),
246 LoggerFactory
::getInstance( 'BlockManager' ),
247 $this->getServiceContainer()->getHookContainer(),
248 $this->getServiceContainer()->getDatabaseBlockStore(),
249 $this->getServiceContainer()->getProxyLookup()
250 ) extends BlockManager
{
251 protected function checkHost( $hostname ) {
255 $this->watchlistManager ??
= $this->getServiceContainer()->getWatchlistManager();
256 $this->hookContainer ??
= new HookContainer(
257 new StaticHookRegistry( [], [], [] ),
260 $this->userNameUtils ??
= $this->getServiceContainer()->getUserNameUtils();
261 $this->loadBalancer ??
= $this->getServiceContainer()->getDBLoadBalancer();
262 $this->contentLanguage ??
= $this->getServiceContainer()->getContentLanguage();
263 $this->languageConverterFactory ??
= $this->getServiceContainer()->getLanguageConverterFactory();
264 $this->botPasswordStore ??
= $this->getServiceContainer()->getBotPasswordStore();
265 $this->userFactory ??
= $this->getServiceContainer()->getUserFactory();
266 $this->userIdentityLookup ??
= $this->getServiceContainer()->getUserIdentityLookup();
267 $this->userOptionsManager ??
= $this->getServiceContainer()->getUserOptionsManager();
268 $this->objectCacheFactory ??
= $this->getServiceContainer()->getObjectCacheFactory();
269 $this->logger ??
= new TestLogger();
271 if ( $regen ||
!$this->config
->has( MainConfigNames
::AuthManagerConfig
) ) {
272 $this->initializeConfig();
275 $this->manager
= new AuthManager(
278 $this->objectFactory
,
279 $this->hookContainer
,
281 $this->userNameUtils
,
283 $this->watchlistManager
,
285 $this->contentLanguage
,
286 $this->languageConverterFactory
,
287 $this->botPasswordStore
,
289 $this->userIdentityLookup
,
290 $this->userOptionsManager
292 $this->manager
->setLogger( $this->logger
);
293 $this->managerPriv
= TestingAccessWrapper
::newFromObject( $this->manager
);
297 * Setup SessionManager with a mock session provider
298 * @param bool|null $canChangeUser If non-null, canChangeUser will be mocked to return this
299 * @param array $methods Additional methods to mock
300 * @return array (MediaWiki\Session\SessionProvider, ScopedCallback)
302 protected function getMockSessionProvider( $canChangeUser = null, array $methods = [] ) {
303 if ( !isset( $this->config
) ) {
304 $this->config
= new HashConfig();
305 $this->initializeConfig();
307 $this->config
->set( MainConfigNames
::ObjectCacheSessionExpiry
, 100 );
309 $methods[] = '__toString';
310 $methods[] = 'describe';
311 if ( $canChangeUser !== null ) {
312 $methods[] = 'canChangeUser';
314 $provider = $this->getMockBuilder( DummySessionProvider
::class )
315 ->onlyMethods( $methods )
317 $provider->method( '__toString' )
318 ->willReturn( 'MockSessionProvider' );
319 $provider->method( 'describe' )
320 ->willReturn( 'MockSessionProvider sessions' );
321 if ( $canChangeUser !== null ) {
322 $provider->method( 'canChangeUser' )
323 ->willReturn( $canChangeUser );
325 $this->config
->set( MainConfigNames
::SessionProviders
, [
326 [ 'factory' => static function () use ( $provider ) {
331 $manager = new SessionManager( [
332 'config' => $this->config
,
333 'logger' => new NullLogger(),
334 'store' => new HashBagOStuff(),
336 TestingAccessWrapper
::newFromObject( $manager )->getProvider( (string)$provider );
338 $reset = TestUtils
::setSessionManagerSingleton( $manager );
340 if ( isset( $this->request
) ) {
341 $manager->getSessionForRequest( $this->request
);
344 return [ $provider, $reset ];
347 public function testCanAuthenticateNow() {
348 $this->initializeManager();
350 [ $provider, $reset ] = $this->getMockSessionProvider( false );
351 $this->assertFalse( $this->manager
->canAuthenticateNow() );
352 ScopedCallback
::consume( $reset );
354 [ $provider, $reset ] = $this->getMockSessionProvider( true );
355 $this->assertTrue( $this->manager
->canAuthenticateNow() );
356 ScopedCallback
::consume( $reset );
359 public function testNormalizeUsername() {
361 $this->createMock( AbstractPrimaryAuthenticationProvider
::class ),
362 $this->createMock( AbstractPrimaryAuthenticationProvider
::class ),
363 $this->createMock( AbstractPrimaryAuthenticationProvider
::class ),
364 $this->createMock( AbstractPrimaryAuthenticationProvider
::class ),
366 foreach ( $mocks as $key => $mock ) {
367 $mock->method( 'getUniqueId' )->willReturn( $key );
369 $mocks[0]->expects( $this->once() )->method( 'providerNormalizeUsername' )
370 ->with( $this->identicalTo( 'XYZ' ) )
371 ->willReturn( 'Foo' );
372 $mocks[1]->expects( $this->once() )->method( 'providerNormalizeUsername' )
373 ->with( $this->identicalTo( 'XYZ' ) )
374 ->willReturn( 'Foo' );
375 $mocks[2]->expects( $this->once() )->method( 'providerNormalizeUsername' )
376 ->with( $this->identicalTo( 'XYZ' ) )
377 ->willReturn( null );
378 $mocks[3]->expects( $this->once() )->method( 'providerNormalizeUsername' )
379 ->with( $this->identicalTo( 'XYZ' ) )
380 ->willReturn( 'Bar!' );
382 $this->primaryauthMocks
= $mocks;
384 $this->initializeManager();
386 $this->assertSame( [ 'Foo', 'Bar!' ], $this->manager
->normalizeUsername( 'XYZ' ) );
390 * @dataProvider provideSecuritySensitiveOperationStatus
391 * @param bool $mutableSession
393 public function testSecuritySensitiveOperationStatus( $mutableSession ) {
394 $this->logger
= new NullLogger();
395 $user = $this->getTestSysop()->getUser();
397 $reauth = $mutableSession ? AuthManager
::SEC_REAUTH
: AuthManager
::SEC_FAIL
;
399 [ $provider, $reset ] = $this->getMockSessionProvider(
400 $mutableSession, [ 'provideSessionInfo' ]
402 $provider->method( 'provideSessionInfo' )
403 ->willReturnCallback( static function () use ( $provider, &$provideUser ) {
404 return new SessionInfo( SessionInfo
::MIN_PRIORITY
, [
405 'provider' => $provider,
406 'id' => DummySessionProvider
::ID
,
408 'userInfo' => UserInfo
::newFromUser( $provideUser, true )
411 $this->initializeManager();
413 $this->config
->set( MainConfigNames
::ReauthenticateTime
, [] );
414 $this->config
->set( MainConfigNames
::AllowSecuritySensitiveOperationIfCannotReauthenticate
, [] );
415 $provideUser = new User
;
416 $session = $provider->getManager()->getSessionForRequest( $this->request
);
417 $this->assertSame( 0, $session->getUser()->getId() );
419 // Anonymous user => reauth
420 $session->set( 'AuthManager:lastAuthId', 0 );
421 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
422 $this->assertSame( $reauth, $this->manager
->securitySensitiveOperationStatus( 'foo' ) );
424 $provideUser = $user;
425 $session = $provider->getManager()->getSessionForRequest( $this->request
);
426 $this->assertSame( $user->getId(), $session->getUser()->getId() );
428 // Error for no default (only gets thrown for non-anonymous user)
429 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
430 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
432 $this->manager
->securitySensitiveOperationStatus( 'foo' );
433 $this->fail( 'Expected exception not thrown' );
434 } catch ( UnexpectedValueException
$ex ) {
437 ?
'$wgReauthenticateTime lacks a default'
438 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
443 if ( $mutableSession ) {
444 $this->config
->set( MainConfigNames
::ReauthenticateTime
, [
450 // Mismatched user ID
451 $session->set( 'AuthManager:lastAuthId', $user->getId() +
1 );
452 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
454 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
457 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
460 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
464 $session->set( 'AuthManager:lastAuthId', $user->getId() );
465 $session->set( 'AuthManager:lastAuthTimestamp', null );
467 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
470 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'test' )
473 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test2' )
476 // Recent enough to pass
477 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
479 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
482 // Not recent enough to pass
483 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
485 AuthManager
::SEC_REAUTH
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
487 // But recent enough for the 'test' operation
489 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'test' )
492 $this->config
->set( MainConfigNames
::AllowSecuritySensitiveOperationIfCannotReauthenticate
, [
498 AuthManager
::SEC_OK
, $this->manager
->securitySensitiveOperationStatus( 'foo' )
502 AuthManager
::SEC_FAIL
, $this->manager
->securitySensitiveOperationStatus( 'test' )
506 // Test hook, all three possible values
508 AuthManager
::SEC_OK
=> AuthManager
::SEC_OK
,
509 AuthManager
::SEC_REAUTH
=> $reauth,
510 AuthManager
::SEC_FAIL
=> AuthManager
::SEC_FAIL
,
511 ] as $hook => $expect ) {
512 $this->hook( 'SecuritySensitiveOperationStatus',
513 SecuritySensitiveOperationStatusHook
::class,
517 /* $status */ $this->anything(),
518 /* $operation */ $this->anything(),
519 /* $session */ $this->callback( static function ( $s ) use ( $session ) {
520 return $s->getId() === $session->getId();
522 /* $timeSinceAuth*/ $mutableSession
523 ?
$this->equalToWithDelta( 500, 2 )
526 ->willReturnCallback( static function ( &$v ) use ( $hook ) {
530 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
532 $expect, $this->manager
->securitySensitiveOperationStatus( 'test' ), "hook $hook"
535 $expect, $this->manager
->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
537 $this->unhook( 'SecuritySensitiveOperationStatus' );
540 ScopedCallback
::consume( $reset );
543 public static function provideSecuritySensitiveOperationStatus() {
551 * @dataProvider provideUserCanAuthenticate
552 * @param bool $primary1Can
553 * @param bool $primary2Can
554 * @param bool $expect
556 public function testUserCanAuthenticate( $primary1Can, $primary2Can, $expect ) {
557 $userName = 'TestUserCanAuthenticate';
558 $mock1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
559 $mock1->method( 'getUniqueId' )
560 ->willReturn( 'primary1' );
561 $mock1->method( 'testUserCanAuthenticate' )
563 ->willReturn( $primary1Can );
564 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
565 $mock2->method( 'getUniqueId' )
566 ->willReturn( 'primary2' );
567 $mock2->method( 'testUserCanAuthenticate' )
569 ->willReturn( $primary2Can );
570 $this->primaryauthMocks
= [ $mock1, $mock2 ];
572 $this->initializeManager( true );
573 $this->assertSame( $expect, $this->manager
->userCanAuthenticate( $userName ) );
576 public static function provideUserCanAuthenticate() {
578 [ false, false, false ],
579 [ true, false, true ],
580 [ false, true, true ],
581 [ true, true, true ],
585 public function testRevokeAccessForUser() {
586 $userName = 'TestRevokeAccessForUser';
587 $this->initializeManager();
589 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
590 $mock->method( 'getUniqueId' )
591 ->willReturn( 'primary' );
592 $mock->expects( $this->once() )->method( 'providerRevokeAccessForUser' )
594 $this->primaryauthMocks
= [ $mock ];
596 $this->initializeManager( true );
597 $this->logger
->setCollect( true );
599 $this->manager
->revokeAccessForUser( $userName );
602 [ LogLevel
::INFO
, 'Revoking access for {user}' ],
603 ], $this->logger
->getBuffer() );
606 public function testProviderCreation() {
608 'pre' => $this->createMock( AbstractPreAuthenticationProvider
::class ),
609 'primary' => $this->createMock( AbstractPrimaryAuthenticationProvider
::class ),
610 'secondary' => $this->createMock( AbstractSecondaryAuthenticationProvider
::class ),
612 foreach ( $mocks as $key => $mock ) {
613 $mock->method( 'getUniqueId' )->willReturn( $key );
614 $mock->expects( $this->once() )->method( 'init' );
616 $this->preauthMocks
= [ $mocks['pre'] ];
617 $this->primaryauthMocks
= [ $mocks['primary'] ];
618 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
621 $this->initializeManager();
624 $this->managerPriv
->getAuthenticationProvider( 'primary' )
628 $this->managerPriv
->getAuthenticationProvider( 'secondary' )
632 $this->managerPriv
->getAuthenticationProvider( 'pre' )
635 [ 'pre' => $mocks['pre'] ],
636 $this->managerPriv
->getPreAuthenticationProviders()
639 [ 'primary' => $mocks['primary'] ],
640 $this->managerPriv
->getPrimaryAuthenticationProviders()
643 [ 'secondary' => $mocks['secondary'] ],
644 $this->managerPriv
->getSecondaryAuthenticationProviders()
648 $mock1 = $this->createMock( AbstractPreAuthenticationProvider
::class );
649 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
650 $mock1->method( 'getUniqueId' )->willReturn( 'X' );
651 $mock2->method( 'getUniqueId' )->willReturn( 'X' );
652 $this->preauthMocks
= [ $mock1 ];
653 $this->primaryauthMocks
= [ $mock2 ];
654 $this->secondaryauthMocks
= [];
655 $this->initializeManager( true );
657 $this->managerPriv
->getAuthenticationProvider( 'Y' );
658 $this->fail( 'Expected exception not thrown' );
659 } catch ( RuntimeException
$ex ) {
660 $class1 = get_class( $mock1 );
661 $class2 = get_class( $mock2 );
663 "Duplicate specifications for id X (classes $class2 and $class1)", $ex->getMessage()
668 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
669 $mock->method( 'getUniqueId' )->willReturn( 'X' );
670 $class = get_class( $mock );
671 $this->preauthMocks
= [ $mock ];
672 $this->primaryauthMocks
= [];
673 $this->secondaryauthMocks
= [];
674 $this->initializeManager( true );
676 $this->managerPriv
->getPreAuthenticationProviders();
677 $this->fail( 'Expected exception not thrown' );
678 } catch ( RuntimeException
$ex ) {
680 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
684 $this->preauthMocks
= [];
685 $this->primaryauthMocks
= [ $mock ];
686 $this->secondaryauthMocks
= [];
687 $this->initializeManager( true );
689 $this->managerPriv
->getPrimaryAuthenticationProviders();
690 $this->fail( 'Expected exception not thrown' );
691 } catch ( RuntimeException
$ex ) {
693 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
697 $this->preauthMocks
= [];
698 $this->primaryauthMocks
= [];
699 $this->secondaryauthMocks
= [ $mock ];
700 $this->initializeManager( true );
702 $this->managerPriv
->getSecondaryAuthenticationProviders();
703 $this->fail( 'Expected exception not thrown' );
704 } catch ( RuntimeException
$ex ) {
706 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
712 $mock1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
713 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
714 $mock3 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
715 $mock1->method( 'getUniqueId' )->willReturn( 'A' );
716 $mock2->method( 'getUniqueId' )->willReturn( 'B' );
717 $mock3->method( 'getUniqueId' )->willReturn( 'C' );
718 $this->preauthMocks
= [];
719 $this->primaryauthMocks
= [ $mock1, $mock2, $mock3 ];
720 $this->secondaryauthMocks
= [];
721 $this->initializeConfig();
722 $config = $this->config
->get( MainConfigNames
::AuthManagerConfig
);
724 $this->initializeManager( false );
726 [ 'A' => $mock1, 'B' => $mock2, 'C' => $mock3 ],
727 $this->managerPriv
->getPrimaryAuthenticationProviders()
730 $config['primaryauth']['A']['sort'] = 100;
731 $config['primaryauth']['C']['sort'] = -1;
732 $this->config
->set( MainConfigNames
::AuthManagerConfig
, $config );
733 $this->initializeManager( false );
735 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
736 $this->managerPriv
->getPrimaryAuthenticationProviders()
740 $mockPreAuth1 = $this->createMock( AbstractPreAuthenticationProvider
::class );
741 $mockPreAuth2 = $this->createMock( AbstractPreAuthenticationProvider
::class );
742 $mockPrimary1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
743 $mockPrimary2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
744 $mockSecondary1 = $this->createMock( AbstractSecondaryAuthenticationProvider
::class );
745 $mockSecondary2 = $this->createMock( AbstractSecondaryAuthenticationProvider
::class );
746 $mockPreAuth1->method( 'getUniqueId' )->willReturn( 'pre1' );
747 $mockPreAuth2->method( 'getUniqueId' )->willReturn( 'pre2' );
748 $mockPrimary1->method( 'getUniqueId' )->willReturn( 'primary1' );
749 $mockPrimary2->method( 'getUniqueId' )->willReturn( 'primary2' );
750 $mockSecondary1->method( 'getUniqueId' )->willReturn( 'secondary1' );
751 $mockSecondary2->method( 'getUniqueId' )->willReturn( 'secondary2' );
752 $this->preauthMocks
= [ $mockPreAuth1, $mockPreAuth2 ];
753 $this->primaryauthMocks
= [ $mockPrimary1, $mockPrimary2 ];
754 $this->secondaryauthMocks
= [ $mockSecondary1, $mockSecondary2 ];
755 $this->initializeConfig();
756 $this->initializeManager( true );
757 $this->hookContainer
->register( 'AuthManagerFilterProviders', static function ( &$providers ) {
758 unset( $providers['preauth']['pre1'] );
759 $providers['primaryauth']['primary2'] = false;
760 $providers['secondaryauth'] = [ 'secondary2' => true ];
762 $this->assertSame( [ 'pre2' => $mockPreAuth2 ], $this->managerPriv
->getPreAuthenticationProviders() );
763 $this->assertSame( [ 'primary1' => $mockPrimary1 ], $this->managerPriv
->getPrimaryAuthenticationProviders() );
764 $this->assertSame( [ 'secondary2' => $mockSecondary2 ], $this->managerPriv
->getSecondaryAuthenticationProviders() );
768 * @dataProvider provideSetDefaultUserOptions
770 public function testSetDefaultUserOptions(
771 $contLang, $useContextLang, $expectedLang, $expectedVariant
773 $this->setContentLang( $contLang );
774 $this->initializeManager( true );
775 $context = RequestContext
::getMain();
776 $reset = new ScopedCallback( [ $context, 'setLanguage' ], [ $context->getLanguage() ] );
777 $context->setLanguage( 'de' );
779 $user = User
::newFromName( self
::usernameForCreation() );
780 $user->addToDatabase();
781 $oldToken = $user->getToken();
782 $this->managerPriv
->setDefaultUserOptions( $user, $useContextLang );
783 $user->saveSettings();
784 $this->assertNotEquals( $oldToken, $user->getToken() );
787 $this->userOptionsManager
->getOption( $user, 'language' )
791 $this->userOptionsManager
->getOption( $user, 'variant' )
795 public static function provideSetDefaultUserOptions() {
797 [ 'zh', false, 'zh', 'zh' ],
798 [ 'zh', true, 'de', 'zh' ],
799 [ 'fr', true, 'de', 'fr' ],
803 public function testForcePrimaryAuthenticationProviders() {
804 $mockA = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
805 $mockB = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
806 $mockB2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
807 $mockA->method( 'getUniqueId' )->willReturn( 'A' );
808 $mockB->method( 'getUniqueId' )->willReturn( 'B' );
809 $mockB2->method( 'getUniqueId' )->willReturn( 'B' );
810 $this->primaryauthMocks
= [ $mockA ];
812 $this->logger
= new TestLogger( true );
814 // Test without first initializing the configured providers
815 $this->initializeManager();
816 $this->expectDeprecationAndContinue( '/AuthManager::forcePrimaryAuthenticationProviders/' );
817 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
819 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
821 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
822 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
824 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
825 ], $this->logger
->getBuffer() );
826 $this->logger
->clearBuffer();
828 // Test with first initializing the configured providers
829 $this->initializeManager();
830 $this->assertSame( $mockA, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
831 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
832 $this->request
->getSession()->setSecret( AuthManager
::AUTHN_STATE
, 'test' );
833 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
, 'test' );
834 $this->expectDeprecationAndContinue( '/AuthManager::forcePrimaryAuthenticationProviders/' );
835 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB ], 'testing' );
837 [ 'B' => $mockB ], $this->managerPriv
->getPrimaryAuthenticationProviders()
839 $this->assertSame( null, $this->managerPriv
->getAuthenticationProvider( 'A' ) );
840 $this->assertSame( $mockB, $this->managerPriv
->getAuthenticationProvider( 'B' ) );
841 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::AUTHN_STATE
) );
843 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
846 [ LogLevel
::WARNING
, 'Overriding AuthManager primary authn because testing' ],
849 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
851 ], $this->logger
->getBuffer() );
852 $this->logger
->clearBuffer();
854 // Test duplicate IDs
855 $this->initializeManager();
857 $this->expectDeprecationAndContinue( '/AuthManager::forcePrimaryAuthenticationProviders/' );
858 $this->manager
->forcePrimaryAuthenticationProviders( [ $mockB, $mockB2 ], 'testing' );
859 $this->fail( 'Expected exception not thrown' );
860 } catch ( RuntimeException
$ex ) {
861 $class1 = get_class( $mockB );
862 $class2 = get_class( $mockB2 );
864 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
869 $mock = $this->getMockForAbstractClass( AuthenticationProvider
::class );
870 $mock->method( 'getUniqueId' )->willReturn( 'X' );
871 $class = get_class( $mock );
873 $this->manager
->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
874 $this->fail( 'Expected exception not thrown' );
875 } catch ( RuntimeException
$ex ) {
877 "Expected instance of MediaWiki\\Auth\\AbstractPrimaryAuthenticationProvider, got $class",
883 public function testBeginAuthentication() {
884 $this->initializeManager();
887 [ $provider, $reset ] = $this->getMockSessionProvider( false );
888 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->never() );
889 $this->request
->getSession()->setSecret( AuthManager
::AUTHN_STATE
, 'test' );
891 $this->manager
->beginAuthentication( [], 'http://localhost/' );
892 $this->fail( 'Expected exception not thrown' );
893 } catch ( LogicException
$ex ) {
894 $this->assertSame( 'Authentication is not possible now', $ex->getMessage() );
896 $this->unhook( 'UserLoggedIn' );
897 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::AUTHN_STATE
) );
898 ScopedCallback
::consume( $reset );
899 $this->initializeManager( true );
901 // CreatedAccountAuthenticationRequest
902 $user = $this->getTestSysop()->getUser();
904 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
906 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->never() );
908 $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
909 $this->fail( 'Expected exception not thrown' );
910 } catch ( LogicException
$ex ) {
912 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
913 'that created the account',
917 $this->unhook( 'UserLoggedIn' );
919 $this->request
->getSession()->clear();
920 $this->request
->getSession()->setSecret( AuthManager
::AUTHN_STATE
, 'test' );
921 $this->managerPriv
->createdAccountAuthenticationRequests
= [ $reqs[0] ];
922 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->once() )
923 ->with( $this->callback( static function ( $u ) use ( $user ) {
924 return $user->getId() === $u->getId() && $user->getName() === $u->getName();
926 $this->hook( 'AuthManagerLoginAuthenticateAudit',
927 AuthManagerLoginAuthenticateAuditHook
::class, $this->once() );
928 $this->logger
->setCollect( true );
929 $ret = $this->manager
->beginAuthentication( $reqs, 'http://localhost/' );
930 $this->logger
->setCollect( false );
931 $this->unhook( 'UserLoggedIn' );
932 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
933 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
934 $this->assertSame( $user->getName(), $ret->username
);
935 $this->assertSame( $user->getId(), $this->request
->getSessionData( 'AuthManager:lastAuthId' ) );
936 // FIXME: Avoid relying on implicit amounts of time elapsing.
937 $this->assertEqualsWithDelta(
939 $this->request
->getSessionData( 'AuthManager:lastAuthTimestamp' ),
943 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::AUTHN_STATE
) );
944 $this->assertSame( $user->getId(), $this->request
->getSession()->getUser()->getId() );
946 [ LogLevel
::INFO
, 'Logging in {user} after account creation' ],
947 ], $this->logger
->getBuffer() );
950 public function testCreateFromLogin() {
951 $user = $this->getTestSysop()->getUser();
952 $req1 = $this->createMock( AuthenticationRequest
::class );
953 $req2 = $this->createMock( AuthenticationRequest
::class );
954 $req3 = $this->createMock( AuthenticationRequest
::class );
955 $userReq = new UsernameAuthenticationRequest
;
956 $userReq->username
= 'UTDummy';
958 $req1->returnToUrl
= 'http://localhost/';
959 $req2->returnToUrl
= 'http://localhost/';
960 $req3->returnToUrl
= 'http://localhost/';
961 $req3->username
= 'UTDummy';
962 $userReq->returnToUrl
= 'http://localhost/';
964 // Passing one into beginAuthentication(), and an immediate FAIL
965 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
966 $this->primaryauthMocks
= [ $primary ];
967 $this->initializeManager( true );
968 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
969 $res->createRequest
= $req1;
970 $primary->method( 'beginPrimaryAuthentication' )
971 ->willReturn( $res );
972 $createReq = new CreateFromLoginAuthenticationRequest(
973 null, [ $req2->getUniqueId() => $req2 ]
975 $this->logger
->setCollect( true );
976 $ret = $this->manager
->beginAuthentication( [ $createReq ], 'http://localhost/' );
977 $this->logger
->setCollect( false );
978 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
979 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
980 $this->assertSame( $req1, $ret->createRequest
->createRequest
);
981 $this->assertEquals( [ $req2->getUniqueId() => $req2 ], $ret->createRequest
->maybeLink
);
983 // UI, then FAIL in beginAuthentication()
984 $primary = $this->getMockBuilder( AbstractPrimaryAuthenticationProvider
::class )
985 ->onlyMethods( [ 'continuePrimaryAuthentication' ] )
986 ->getMockForAbstractClass();
987 $this->primaryauthMocks
= [ $primary ];
988 $this->initializeManager( true );
989 $primary->method( 'beginPrimaryAuthentication' )
990 ->willReturn( AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) ) );
991 $res = AuthenticationResponse
::newFail( wfMessage( 'foo' ) );
992 $res->createRequest
= $req2;
993 $primary->method( 'continuePrimaryAuthentication' )
994 ->willReturn( $res );
995 $this->logger
->setCollect( true );
996 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
997 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
998 $ret = $this->manager
->continueAuthentication( [] );
999 $this->logger
->setCollect( false );
1000 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1001 $this->assertInstanceOf( CreateFromLoginAuthenticationRequest
::class, $ret->createRequest
);
1002 $this->assertSame( $req2, $ret->createRequest
->createRequest
);
1003 $this->assertEquals( [], $ret->createRequest
->maybeLink
);
1005 // Pass into beginAccountCreation(), see that maybeLink and createRequest get copied
1006 $primary = $this->getMockForAbstractClass( AbstractPrimaryAuthenticationProvider
::class );
1007 $this->primaryauthMocks
= [ $primary ];
1008 $this->initializeManager( true );
1009 $createReq = new CreateFromLoginAuthenticationRequest( $req3, [ $req2 ] );
1010 $createReq->returnToUrl
= 'http://localhost/';
1011 $createReq->username
= 'UTDummy';
1012 $res = AuthenticationResponse
::newUI( [ $req1 ], wfMessage( 'foo' ) );
1013 $primary->method( 'beginPrimaryAccountCreation' )
1014 ->with( $this->anything(), $this->anything(), [ $userReq, $createReq, $req3 ] )
1015 ->willReturn( $res );
1016 $primary->method( 'accountCreationType' )
1017 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
1018 $this->logger
->setCollect( true );
1019 $ret = $this->manager
->beginAccountCreation(
1020 $user, [ $userReq, $createReq ], 'http://localhost/'
1022 $this->logger
->setCollect( false );
1023 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
1024 $state = $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
);
1025 $this->assertNotNull( $state );
1026 $this->assertEquals( [ $userReq, $createReq, $req3 ], $state['reqs'] );
1027 $this->assertEquals( [ $req2 ], $state['maybeLink'] );
1031 * @dataProvider provideAuthentication
1032 * @param StatusValue $preResponse
1033 * @param array<AuthenticationResponse|Exception> $primaryResponses
1034 * @param array<AuthenticationResponse|Exception> $secondaryResponses
1035 * @param array<AuthenticationResponse|Exception> $managerResponses
1036 * @param bool $link Whether the primary authentication provider is a "link" provider
1038 public function testAuthentication(
1039 StatusValue
$preResponse, array $primaryResponses, array $secondaryResponses,
1040 array $managerResponses, $link = false
1042 $this->initializeManager();
1043 $user = $this->getTestSysop()->getUser();
1044 $id = $user->getId();
1045 $name = $user->getName();
1046 // Hack: replace placeholder usernames with that of the test user. A better solution would be to instantiate
1047 // all responses here, only providing constructor arguments (like the status) from the data provider.
1048 $responseArrays = [ $primaryResponses, $secondaryResponses, $managerResponses ];
1049 foreach ( $responseArrays as $respArray ) {
1050 foreach ( $respArray as $resp ) {
1051 if ( $resp instanceof AuthenticationResponse
&& $resp->username
=== 'PLACEHOLDER' ) {
1052 $resp->username
= $name;
1057 // Set up lots of mocks...
1058 $req = new RememberMeAuthenticationRequest
;
1059 $req->rememberMe
= (bool)rand( 0, 1 );
1061 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
1062 $class = ucfirst( $key ) . 'AuthenticationProvider';
1063 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\Abstract$class" )
1064 ->setMockClassName( "MockAbstract$class" )
1066 $mocks[$key]->method( 'getUniqueId' )
1067 ->willReturn( $key );
1068 $mocks[$key . '2'] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
1069 $mocks[$key . '2']->method( 'getUniqueId' )
1070 ->willReturn( $key . '2' );
1071 $mocks[$key . '3'] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
1072 $mocks[$key . '3']->method( 'getUniqueId' )
1073 ->willReturn( $key . '3' );
1075 foreach ( $mocks as $mock ) {
1076 $mock->method( 'getAuthenticationRequests' )
1080 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
1081 ->willReturnCallback( function ( $reqs ) use ( $req, $preResponse ) {
1082 $this->assertContains( $req, $reqs );
1083 return $preResponse;
1086 $ct = count( $primaryResponses );
1087 $callback = $this->returnCallback( function ( $reqs ) use ( $req, &$primaryResponses ) {
1088 $this->assertContains( $req, $reqs );
1089 return array_shift( $primaryResponses );
1091 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
1092 ->method( 'beginPrimaryAuthentication' )
1093 ->will( $callback );
1094 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1095 ->method( 'continuePrimaryAuthentication' )
1096 ->will( $callback );
1098 $mocks['primary']->method( 'accountCreationType' )
1099 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
1102 $ct = count( $secondaryResponses );
1103 $callback = $this->returnCallback( function ( $user, $reqs ) use ( $id, $name, $req, &$secondaryResponses ) {
1104 $this->assertSame( $id, $user->getId() );
1105 $this->assertSame( $name, $user->getName() );
1106 $this->assertContains( $req, $reqs );
1107 return array_shift( $secondaryResponses );
1109 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
1110 ->method( 'beginSecondaryAuthentication' )
1111 ->will( $callback );
1112 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
1113 ->method( 'continueSecondaryAuthentication' )
1114 ->will( $callback );
1116 $abstain = AuthenticationResponse
::newAbstain();
1117 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAuthentication' )
1118 ->willReturn( StatusValue
::newGood() );
1119 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAuthentication' )
1120 ->willReturn( $abstain );
1121 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAuthentication' );
1122 $mocks['secondary2']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
1123 ->willReturn( $abstain );
1124 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
1125 $mocks['secondary3']->expects( $this->atMost( 1 ) )->method( 'beginSecondaryAuthentication' )
1126 ->willReturn( $abstain );
1127 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAuthentication' );
1129 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
1130 $this->primaryauthMocks
= [ $mocks['primary'], $mocks['primary2'] ];
1131 $this->secondaryauthMocks
= [
1132 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2'],
1133 // So linking happens
1134 new ConfirmLinkSecondaryAuthenticationProvider
,
1136 $this->initializeManager( true );
1137 $this->logger
->setCollect( true );
1139 $constraint = Assert
::logicalOr(
1140 $this->equalTo( AuthenticationResponse
::PASS
),
1141 $this->equalTo( AuthenticationResponse
::FAIL
)
1143 $providers = array_filter(
1145 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
1147 static function ( $p ) {
1148 return is_callable( [ $p, 'expects' ] );
1151 foreach ( $providers as $p ) {
1152 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', false );
1153 $p->expects( $this->atMost( 1 ) )->method( 'postAuthentication' )
1154 ->willReturnCallback( function ( $userArg, $response ) use ( $user, $constraint, $p ) {
1155 if ( $userArg !== null ) {
1156 $this->assertInstanceOf( User
::class, $userArg );
1157 $this->assertSame( $user->getName(), $userArg->getName() );
1159 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
1160 $this->assertThat( $response->status
, $constraint );
1161 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', $response->status
);
1165 $session = $this->request
->getSession();
1166 $session->setRememberUser( !$req->rememberMe
);
1168 foreach ( $managerResponses as $i => $response ) {
1169 $success = $response instanceof AuthenticationResponse
&&
1170 $response->status
=== AuthenticationResponse
::PASS
;
1172 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->once() )
1173 ->with( $this->callback( static function ( $user ) use ( $id, $name ) {
1174 return $user->getId() === $id && $user->getName() === $name;
1177 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->never() );
1180 $response instanceof AuthenticationResponse
&&
1181 $response->status
=== AuthenticationResponse
::FAIL
&&
1182 $response->message
->getKey() !== 'authmanager-authn-not-in-progress' &&
1183 $response->message
->getKey() !== 'authmanager-authn-no-primary'
1186 $this->hook( 'AuthManagerLoginAuthenticateAudit',
1187 AuthManagerLoginAuthenticateAuditHook
::class, $this->once() );
1189 $this->hook( 'AuthManagerLoginAuthenticateAudit',
1190 AuthManagerLoginAuthenticateAuditHook
::class, $this->never() );
1195 $ret = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1197 $ret = $this->manager
->continueAuthentication( [ $req ] );
1199 if ( $response instanceof Exception
) {
1200 $this->fail( 'Expected exception not thrown', "Response $i" );
1202 } catch ( Exception
$ex ) {
1203 if ( !$response instanceof Exception
) {
1206 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
1207 $this->assertNull( $session->getSecret( AuthManager
::AUTHN_STATE
),
1208 "Response $i, exception, session state" );
1209 $this->unhook( 'UserLoggedIn' );
1210 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1214 $this->unhook( 'UserLoggedIn' );
1215 $this->unhook( 'AuthManagerLoginAuthenticateAudit' );
1217 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
1219 $ret->message
= $this->message( $ret->message
);
1220 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
1222 $this->assertSame( $id, $session->getUser()->getId(),
1223 "Response $i, authn" );
1225 $this->assertSame( 0, $session->getUser()->getId(),
1226 "Response $i, authn" );
1228 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
1229 $this->assertNull( $session->getSecret( AuthManager
::AUTHN_STATE
),
1230 "Response $i, session state" );
1231 foreach ( $providers as $p ) {
1232 $this->assertSame( $response->status
, DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ),
1233 "Response $i, post-auth callback called" );
1236 $this->assertNotNull( $session->getSecret( AuthManager
::AUTHN_STATE
),
1237 "Response $i, session state" );
1238 foreach ( $ret->neededRequests
as $neededReq ) {
1239 $this->assertEquals( AuthManager
::ACTION_LOGIN
, $neededReq->action
,
1240 "Response $i, neededRequest action" );
1242 $this->assertEquals(
1243 $ret->neededRequests
,
1244 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN_CONTINUE
),
1245 "Response $i, continuation check"
1247 foreach ( $providers as $p ) {
1248 $this->assertFalse( DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ), "Response $i, post-auth callback not called" );
1252 $state = $session->getSecret( AuthManager
::AUTHN_STATE
);
1253 $maybeLink = $state['maybeLink'] ??
[];
1254 if ( $link && $response->status
=== AuthenticationResponse
::RESTART
) {
1255 $this->assertEquals(
1256 $response->createRequest
->maybeLink
,
1258 "Response $i, maybeLink"
1261 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1266 $this->assertSame( $req->rememberMe
, $session->shouldRememberUser(),
1267 'rememberMe checkbox had effect' );
1269 $this->assertNotSame( $req->rememberMe
, $session->shouldRememberUser(),
1270 'rememberMe checkbox wasn\'t applied' );
1274 public function provideAuthentication() {
1275 $rememberReq = new RememberMeAuthenticationRequest
;
1276 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
1278 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1279 $restartResponse = AuthenticationResponse
::newRestart(
1280 $this->message( 'authmanager-authn-no-local-user' )
1282 $restartResponse->neededRequests
= [ $rememberReq ];
1284 $restartResponse2Pass = AuthenticationResponse
::newPass( null );
1285 $restartResponse2Pass->linkRequest
= $req;
1286 $restartResponse2 = AuthenticationResponse
::newRestart(
1287 $this->message( 'authmanager-authn-no-local-user-link' )
1289 $restartResponse2->createRequest
= new CreateFromLoginAuthenticationRequest(
1290 null, [ $req->getUniqueId() => $req ]
1292 $restartResponse2->createRequest
->action
= AuthManager
::ACTION_LOGIN
;
1293 $restartResponse2->neededRequests
= [ $rememberReq, $restartResponse2->createRequest
];
1295 // Hack: use a placeholder that will be replaced with the actual username in the test method.
1296 $userNamePlaceholder = 'PLACEHOLDER';
1299 'Failure in pre-auth' => [
1300 StatusValue
::newFatal( 'fail-from-pre' ),
1304 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
1305 AuthenticationResponse
::newFail(
1306 $this->message( 'authmanager-authn-not-in-progress' )
1310 'Failure in primary' => [
1311 StatusValue
::newGood(),
1313 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
1318 'All primary abstain' => [
1319 StatusValue
::newGood(),
1321 AuthenticationResponse
::newAbstain(),
1325 AuthenticationResponse
::newFail( $this->message( 'authmanager-authn-no-primary' ) )
1328 'Primary UI, then redirect, then fail' => [
1329 StatusValue
::newGood(),
1331 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1332 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1333 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
1338 'Primary redirect, then abstain' => [
1339 StatusValue
::newGood(),
1341 $tmp = AuthenticationResponse
::newRedirect(
1342 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1344 AuthenticationResponse
::newAbstain(),
1349 new DomainException(
1350 'MockAbstractPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN'
1354 'Primary UI, then pass with no local user' => [
1355 StatusValue
::newGood(),
1357 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1358 AuthenticationResponse
::newPass( null ),
1366 'Primary UI, then pass with no local user (link type)' => [
1367 StatusValue
::newGood(),
1369 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1370 $restartResponse2Pass,
1379 'Primary pass with invalid username' => [
1380 StatusValue
::newGood(),
1382 AuthenticationResponse
::newPass( '<>' ),
1386 new DomainException(
1387 'MockAbstractPrimaryAuthenticationProvider returned an invalid username: <>'
1391 'Secondary fail' => [
1392 StatusValue
::newGood(),
1394 AuthenticationResponse
::newPass( $userNamePlaceholder ),
1397 AuthenticationResponse
::newFail( $this->message( 'fail-in-secondary' ) ),
1401 'Secondary UI, then abstain' => [
1402 StatusValue
::newGood(),
1404 AuthenticationResponse
::newPass( $userNamePlaceholder ),
1407 $tmp = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
1408 AuthenticationResponse
::newAbstain()
1412 AuthenticationResponse
::newPass( $userNamePlaceholder ),
1415 'Secondary pass' => [
1416 StatusValue
::newGood(),
1418 AuthenticationResponse
::newPass( $userNamePlaceholder ),
1421 AuthenticationResponse
::newPass()
1424 AuthenticationResponse
::newPass( $userNamePlaceholder ),
1430 public function testAuthentication_AuthManagerVerifyAuthentication() {
1431 $this->logger
= new NullLogger();
1432 $this->initializeManager();
1435 'getUniqueId' => 'primary',
1436 'testUserForCreation' => StatusValue
::newGood(),
1437 'getAuthenticationRequests' => [],
1438 'beginPrimaryAuthentication' => AuthenticationResponse
::newPass( 'UTDummy' ),
1440 $secondaryConfig = [
1441 'getUniqueId' => 'secondary',
1442 'testUserForCreation' => StatusValue
::newGood(),
1443 'getAuthenticationRequests' => [],
1444 'beginSecondaryAuthentication' => AuthenticationResponse
::newAbstain(),
1446 $updateManager = function () use ( &$primaryConfig, &$secondaryConfig ) {
1447 $primaryMock = $this->createConfiguredMock( AbstractPrimaryAuthenticationProvider
::class, $primaryConfig );
1448 foreach ( [ 'beginPrimaryAuthentication', 'continuePrimaryAuthentication' ] as $method ) {
1449 $primaryMock->expects(
1450 array_key_exists( $method, $primaryConfig ) ?
$this->once() : $this->never()
1451 )->method( $method );
1453 $secondaryMock = $this->createConfiguredMock( AbstractSecondaryAuthenticationProvider
::class, $secondaryConfig );
1454 foreach ( [ 'beginSecondaryAuthentication', 'continueSecondaryAuthentication' ] as $method ) {
1455 $secondaryMock->expects(
1456 array_key_exists( $method, $secondaryConfig ) ?
$this->once() : $this->never()
1457 )->method( $method );
1459 $this->primaryauthMocks
= [ $primaryMock ];
1460 $this->secondaryauthMocks
= [ $secondaryMock ];
1461 $this->initializeManager( true );
1463 $req = new RememberMeAuthenticationRequest();
1464 $req->rememberMe
= true;
1466 // Gets expected data
1468 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1469 $hook->willReturnCallback( function ( $user, &$response, $authManager, $info ) {
1470 $this->assertSame( 'UTDummy', $user->getName() );
1471 $this->assertSame( AuthenticationResponse
::PASS
, $response->status
);
1472 $this->assertSame( $this->manager
, $authManager );
1473 $this->assertSame( AuthManager
::ACTION_LOGIN
, $info['action'] );
1474 $this->assertSame( 'primary', $info['primaryId'] );
1476 $response = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1477 $this->assertEquals( AuthenticationResponse
::newPass( 'UTDummy' ), $response );
1478 $this->assertNotNull( $this->manager
->getRequest()->getSession()->getUser() );
1479 $this->assertSame( 'UTDummy', $this->manager
->getRequest()->getSession()->getUser()->getName() );
1480 $this->unhook( 'AuthManagerVerifyAuthentication' );
1482 // Will prevent login
1484 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1485 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1486 $response = AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) );
1489 $response = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1490 $this->assertEquals( AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) ), $response );
1491 $this->assertTrue( $this->manager
->getRequest()->getSession()->getUser()->isAnon() );
1492 $this->unhook( 'AuthManagerVerifyAuthentication' );
1494 // Will not allow invalid responses
1496 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1497 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1498 $response = 'invalid';
1502 $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1503 $this->fail( 'Expected exception not thrown' );
1504 } catch ( LogicException
$ex ) {
1505 $this->assertSame( '$response must be an AuthenticationResponse', $ex->getMessage() );
1507 $this->unhook( 'AuthManagerVerifyAuthentication' );
1510 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1511 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1512 $response = AuthenticationResponse
::newPass( 'UTDummy' );
1515 $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1516 $this->fail( 'Expected exception not thrown' );
1517 } catch ( LogicException
$ex ) {
1518 $this->assertSame( 'AuthManagerVerifyAuthenticationHook must not modify the response unless it returns false', $ex->getMessage() );
1520 $this->unhook( 'AuthManagerVerifyAuthentication' );
1523 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1524 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1528 $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1529 $this->fail( 'Expected exception not thrown' );
1530 } catch ( LogicException
$ex ) {
1531 $this->assertSame( 'AuthManagerVerifyAuthenticationHook must set the response to FAIL if it returns false', $ex->getMessage() );
1533 $this->unhook( 'AuthManagerVerifyAuthentication' );
1535 // Will prevent restart
1536 $primaryConfig['beginPrimaryAuthentication'] = AuthenticationResponse
::newPass( null );
1537 unset( $secondaryConfig['beginSecondaryAuthentication'] );
1539 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
1540 $hook->willReturnCallback( function ( $user, &$response, $authManager, $info ) {
1541 $this->assertSame( AuthenticationResponse
::RESTART
, $response->status
);
1542 $response = AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) );
1545 $response = $this->manager
->beginAuthentication( [ $req ], 'http://localhost/' );
1546 $this->assertEquals( AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) ), $response );
1547 $this->assertTrue( $this->manager
->getRequest()->getSession()->getUser()->isAnon() );
1548 $this->assertNull( $this->manager
->getRequest()->getSession()->get( AuthManager
::AUTHN_STATE
) );
1549 $this->unhook( 'AuthManagerVerifyAuthentication' );
1553 * @dataProvider provideUserExists
1554 * @param bool $primary1Exists
1555 * @param bool $primary2Exists
1556 * @param bool $expect
1558 public function testUserExists( $primary1Exists, $primary2Exists, $expect ) {
1559 $userName = 'TestUserExists';
1560 $mock1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1561 $mock1->method( 'getUniqueId' )
1562 ->willReturn( 'primary1' );
1563 $mock1->method( 'testUserExists' )
1565 ->willReturn( $primary1Exists );
1566 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1567 $mock2->method( 'getUniqueId' )
1568 ->willReturn( 'primary2' );
1569 $mock2->method( 'testUserExists' )
1571 ->willReturn( $primary2Exists );
1572 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1574 $this->initializeManager( true );
1575 $this->assertSame( $expect, $this->manager
->userExists( $userName ) );
1578 public static function provideUserExists() {
1580 [ false, false, false ],
1581 [ true, false, true ],
1582 [ false, true, true ],
1583 [ true, true, true ],
1588 * @dataProvider provideAllowsAuthenticationDataChange
1589 * @param StatusValue $primaryReturn
1590 * @param StatusValue $secondaryReturn
1591 * @param Status $expect
1593 public function testAllowsAuthenticationDataChange( $primaryReturn, $secondaryReturn, $expect ) {
1594 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1596 $mock1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1597 $mock1->method( 'getUniqueId' )->willReturn( '1' );
1598 $mock1->method( 'providerAllowsAuthenticationDataChange' )
1600 ->willReturn( $primaryReturn );
1601 $mock2 = $this->createMock( AbstractSecondaryAuthenticationProvider
::class );
1602 $mock2->method( 'getUniqueId' )->willReturn( '2' );
1603 $mock2->method( 'providerAllowsAuthenticationDataChange' )
1605 ->willReturn( $secondaryReturn );
1607 $this->primaryauthMocks
= [ $mock1 ];
1608 $this->secondaryauthMocks
= [ $mock2 ];
1609 $this->initializeManager( true );
1610 $this->assertEquals( $expect, $this->manager
->allowsAuthenticationDataChange( $req ) );
1613 public static function provideAllowsAuthenticationDataChange() {
1614 $ignored = Status
::newGood( 'ignored' );
1615 $ignored->warning( 'authmanager-change-not-supported' );
1617 $okFromPrimary = StatusValue
::newGood();
1618 $okFromPrimary->warning( 'warning-from-primary' );
1619 $okFromSecondary = StatusValue
::newGood();
1620 $okFromSecondary->warning( 'warning-from-secondary' );
1622 $throttledMailPassword = StatusValue
::newFatal( 'throttled-mailpassword' );
1626 StatusValue
::newGood(),
1627 StatusValue
::newGood(),
1631 StatusValue
::newGood(),
1632 StatusValue
::newGood( 'ignore' ),
1636 StatusValue
::newGood( 'ignored' ),
1637 StatusValue
::newGood(),
1641 StatusValue
::newGood( 'ignored' ),
1642 StatusValue
::newGood( 'ignored' ),
1646 StatusValue
::newFatal( 'fail from primary' ),
1647 StatusValue
::newGood(),
1648 Status
::newFatal( 'fail from primary' ),
1652 StatusValue
::newGood(),
1653 Status
::wrap( $okFromPrimary ),
1656 StatusValue
::newGood(),
1657 StatusValue
::newFatal( 'fail from secondary' ),
1658 Status
::newFatal( 'fail from secondary' ),
1661 StatusValue
::newGood(),
1663 Status
::wrap( $okFromSecondary ),
1666 StatusValue
::newGood(),
1667 $throttledMailPassword,
1668 Status
::newGood( 'throttled-mailpassword' ),
1673 public function testChangeAuthenticationData() {
1674 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
1675 $req->username
= 'TestChangeAuthenticationData';
1677 $mock1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1678 $mock1->method( 'getUniqueId' )->willReturn( '1' );
1679 $mock1->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1681 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1682 $mock2->method( 'getUniqueId' )->willReturn( '2' );
1683 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1686 $this->primaryauthMocks
= [ $mock1, $mock2 ];
1687 $this->initializeManager( true );
1688 $this->logger
->setCollect( true );
1689 $this->manager
->changeAuthenticationData( $req );
1690 $this->assertSame( [
1691 [ LogLevel
::INFO
, 'Changing authentication data for {user} class {what}' ],
1692 ], $this->logger
->getBuffer() );
1695 public function testCanCreateAccounts() {
1697 PrimaryAuthenticationProvider
::TYPE_CREATE
=> true,
1698 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
1699 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
1702 foreach ( $types as $type => $can ) {
1703 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1704 $mock->method( 'getUniqueId' )->willReturn( $type );
1705 $mock->method( 'accountCreationType' )
1706 ->willReturn( $type );
1707 $this->primaryauthMocks
= [ $mock ];
1708 $this->initializeManager( true );
1709 $this->assertSame( $can, $this->manager
->canCreateAccounts(), $type );
1714 * @covers \MediaWiki\Auth\AuthManager::probablyCanCreateAccount()
1716 public function testProbablyCanCreateAccount() {
1717 $this->setGroupPermissions( '*', 'createaccount', true );
1718 $this->initializeManager( true );
1719 $this->assertEquals(
1720 StatusValue
::newGood(),
1721 $this->manager
->probablyCanCreateAccount( new User
)
1726 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1728 public function testAuthorizeCreateAccount_anon() {
1729 $this->setGroupPermissions( '*', 'createaccount', true );
1730 $this->initializeManager( true );
1731 $this->assertEquals(
1732 StatusValue
::newGood(),
1733 $this->manager
->authorizeCreateAccount( new User
)
1738 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1740 public function testAuthorizeCreateAccount_anonNotAllowed() {
1741 $this->setGroupPermissions( '*', 'createaccount', false );
1742 $this->initializeManager( true );
1743 $status = $this->manager
->authorizeCreateAccount( new User
);
1744 $this->assertStatusError( 'badaccess-groups', $status );
1748 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1750 public function testAuthorizeCreateAccount_readOnly() {
1751 $this->initializeManager( true );
1752 $readOnlyMode = $this->getServiceContainer()->getReadOnlyMode();
1753 $readOnlyMode->setReason( 'Because' );
1754 $this->assertEquals(
1755 StatusValue
::newFatal( wfMessage( 'readonlytext', 'Because' ) ),
1756 $this->manager
->authorizeCreateAccount( new User
)
1758 $readOnlyMode->setReason( false );
1762 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1763 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock()
1765 public function testAuthorizeCreateAccount_blocked() {
1766 $this->initializeManager( true );
1768 $user = User
::newFromName( 'UTBlockee' );
1769 if ( $user->getId() == 0 ) {
1770 $user->addToDatabase();
1771 TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1772 $user->saveSettings();
1774 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1777 'by' => $this->getTestSysop()->getUser(),
1778 'reason' => __METHOD__
,
1779 'expiry' => time() +
100500,
1780 'createAccount' => true,
1782 $block = new DatabaseBlock( $blockOptions );
1783 $blockStore->insertBlock( $block );
1784 $this->resetServices();
1785 $this->initializeManager( true );
1786 $status = $this->manager
->authorizeCreateAccount( $user );
1787 $this->assertStatusError( 'blockedtext', $status );
1791 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1792 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock()
1794 public function testAuthorizeCreateAccount_ipBlocked() {
1795 $this->setGroupPermissions( '*', 'createaccount', true );
1796 $this->initializeManager( true );
1797 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1799 'address' => '127.0.0.0/24',
1800 'by' => $this->getTestSysop()->getUser(),
1801 'reason' => __METHOD__
,
1802 'expiry' => time() +
100500,
1803 'createAccount' => true,
1804 'sitewide' => false,
1806 $block = new DatabaseBlock( $blockOptions );
1807 $blockStore->insertBlock( $block );
1808 $status = $this->manager
->authorizeCreateAccount( new User
);
1809 $this->assertStatusError( 'blockedtext-partial', $status );
1813 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1815 public function testAuthorizeCreateAccount_DNSBlacklist() {
1816 $this->overrideConfigValues( [
1817 MainConfigNames
::EnableDnsBlacklist
=> true,
1818 MainConfigNames
::DnsBlacklistUrls
=> [
1821 MainConfigNames
::ProxyWhitelist
=> [],
1823 unset( $this->blockManager
);
1824 $this->initializeManager( true );
1826 $status = $this->manager
->authorizeCreateAccount( new User
);
1827 $this->assertStatusError( 'sorbs_create_account_reason', $status );
1829 $this->overrideConfigValue( MainConfigNames
::ProxyWhitelist
, [ '127.0.0.1' ] );
1830 unset( $this->blockManager
);
1831 $this->initializeManager( true );
1832 $status = $this->manager
->authorizeCreateAccount( new User
);
1833 $this->assertStatusGood( $status );
1835 unset( $this->blockManager
);
1839 * @covers \MediaWiki\Auth\AuthManager::authorizeCreateAccount()
1840 * @covers \MediaWiki\Permissions\PermissionManager::checkUserBlock()
1842 public function testAuthorizeCreateAccount_ipIsBlockedByUserNot() {
1843 $this->initializeManager( true );
1845 $user = User
::newFromName( 'UTBlockee' );
1846 if ( $user->getId() == 0 ) {
1847 $user->addToDatabase();
1848 TestUser
::setPasswordForUser( $user, 'UTBlockeePassword' );
1849 $user->saveSettings();
1851 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1854 'by' => $this->getTestSysop()->getUser(),
1855 'reason' => __METHOD__
,
1856 'expiry' => time() +
100500,
1857 'createAccount' => false,
1859 $block = new DatabaseBlock( $blockOptions );
1860 $blockStore->insertBlock( $block );
1863 'address' => '127.0.0.0/24',
1864 'by' => $this->getTestSysop()->getUser(),
1865 'reason' => __METHOD__
,
1866 'expiry' => time() +
100500,
1867 'createAccount' => true,
1868 'sitewide' => false,
1870 $block = new DatabaseBlock( $blockOptions );
1871 $blockStore->insertBlock( $block );
1873 $this->resetServices();
1874 $this->initializeManager( true );
1875 $status = $this->manager
->authorizeCreateAccount( $user );
1876 $this->assertStatusError( 'blockedtext-partial', $status );
1880 * @param string $uniq
1883 private static function usernameForCreation( $uniq = '' ) {
1886 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++
$i;
1887 } while ( User
::newFromName( $username )->getId() !== 0 );
1891 public function testCanCreateAccount() {
1892 $username = self
::usernameForCreation();
1893 $this->initializeManager();
1895 $this->assertEquals(
1896 Status
::newFatal( 'authmanager-create-disabled' ),
1897 $this->manager
->canCreateAccount( $username )
1900 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1901 $mock->method( 'getUniqueId' )->willReturn( 'X' );
1902 $mock->method( 'accountCreationType' )
1903 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
1904 $mock->method( 'testUserExists' )->willReturn( true );
1905 $mock->method( 'testUserForCreation' )
1906 ->willReturn( StatusValue
::newGood() );
1907 $this->primaryauthMocks
= [ $mock ];
1908 $this->initializeManager( true );
1910 $this->assertEquals(
1911 Status
::newFatal( 'userexists' ),
1912 $this->manager
->canCreateAccount( $username )
1915 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1916 $mock->method( 'getUniqueId' )->willReturn( 'X' );
1917 $mock->method( 'accountCreationType' )
1918 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
1919 $mock->method( 'testUserExists' )->willReturn( false );
1920 $mock->method( 'testUserForCreation' )
1921 ->willReturn( StatusValue
::newGood() );
1922 $this->primaryauthMocks
= [ $mock ];
1923 $this->initializeManager( true );
1925 $this->assertEquals(
1926 Status
::newFatal( 'noname' ),
1927 $this->manager
->canCreateAccount( $username . '<>' )
1930 $existingUserName = $this->getTestSysop()->getUserIdentity()->getName();
1931 $this->assertEquals(
1932 Status
::newFatal( 'userexists' ),
1933 $this->manager
->canCreateAccount( $existingUserName )
1936 $this->assertEquals(
1938 $this->manager
->canCreateAccount( $username )
1941 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1942 $mock->method( 'getUniqueId' )->willReturn( 'X' );
1943 $mock->method( 'accountCreationType' )
1944 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
1945 $mock->method( 'testUserExists' )->willReturn( false );
1946 $mock->method( 'testUserForCreation' )
1947 ->willReturn( StatusValue
::newFatal( 'fail' ) );
1948 $this->primaryauthMocks
= [ $mock ];
1949 $this->initializeManager( true );
1951 $this->assertEquals(
1952 Status
::newFatal( 'fail' ),
1953 $this->manager
->canCreateAccount( $username )
1957 public function testBeginAccountCreation() {
1958 $creator = $this->getTestSysop()->getUser();
1959 $userReq = new UsernameAuthenticationRequest
;
1960 $this->logger
= new TestLogger( false, static function ( $message, $level ) {
1961 return $level === LogLevel
::DEBUG ?
null : $message;
1963 $this->initializeManager();
1965 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
, 'test' );
1966 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
1968 $this->manager
->beginAccountCreation(
1969 $creator, [], 'http://localhost/'
1971 $this->fail( 'Expected exception not thrown' );
1972 } catch ( LogicException
$ex ) {
1973 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
1975 $this->unhook( 'LocalUserCreated' );
1977 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
1980 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
1981 $mock->method( 'getUniqueId' )->willReturn( 'X' );
1982 $mock->method( 'accountCreationType' )
1983 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
1984 $mock->method( 'testUserExists' )->willReturn( true );
1985 $mock->method( 'testUserForCreation' )
1986 ->willReturn( StatusValue
::newGood() );
1987 $this->primaryauthMocks
= [ $mock ];
1988 $this->initializeManager( true );
1990 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
1991 $ret = $this->manager
->beginAccountCreation( $creator, [], 'http://localhost/' );
1992 $this->unhook( 'LocalUserCreated' );
1993 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
1994 $this->assertSame( 'noname', $ret->message
->getKey() );
1996 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
1997 $userReq->username
= self
::usernameForCreation();
1998 $userReq2 = new UsernameAuthenticationRequest
;
1999 $userReq2->username
= $userReq->username
. 'X';
2000 $ret = $this->manager
->beginAccountCreation(
2001 $creator, [ $userReq, $userReq2 ], 'http://localhost/'
2003 $this->unhook( 'LocalUserCreated' );
2004 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2005 $this->assertSame( 'noname', $ret->message
->getKey() );
2007 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2008 $readOnlyMode = $this->getServiceContainer()->getReadOnlyMode();
2009 $readOnlyMode->setReason( 'Because' );
2010 $userReq->username
= self
::usernameForCreation();
2011 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
2012 $this->unhook( 'LocalUserCreated' );
2013 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2014 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
2015 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
2016 $readOnlyMode->setReason( false );
2018 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2019 $userReq->username
= self
::usernameForCreation();
2020 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
2021 $this->unhook( 'LocalUserCreated' );
2022 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2023 $this->assertSame( 'userexists', $ret->message
->getKey() );
2025 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
2026 $mock->method( 'getUniqueId' )->willReturn( 'X' );
2027 $mock->method( 'accountCreationType' )
2028 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2029 $mock->method( 'testUserExists' )->willReturn( false );
2030 $mock->method( 'testUserForCreation' )
2031 ->willReturn( StatusValue
::newFatal( 'fail' ) );
2032 $this->primaryauthMocks
= [ $mock ];
2033 $this->initializeManager( true );
2035 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2036 $userReq->username
= self
::usernameForCreation();
2037 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
2038 $this->unhook( 'LocalUserCreated' );
2039 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2040 $this->assertSame( 'fail', $ret->message
->getKey() );
2042 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
2043 $mock->method( 'getUniqueId' )->willReturn( 'X' );
2044 $mock->method( 'accountCreationType' )
2045 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2046 $mock->method( 'testUserExists' )->willReturn( false );
2047 $mock->method( 'testUserForCreation' )
2048 ->willReturn( StatusValue
::newGood() );
2049 $this->primaryauthMocks
= [ $mock ];
2050 $this->initializeManager( true );
2052 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2053 $userReq->username
= self
::usernameForCreation() . '<>';
2054 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
2055 $this->unhook( 'LocalUserCreated' );
2056 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2057 $this->assertSame( 'noname', $ret->message
->getKey() );
2059 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2060 $userReq->username
= $creator->getName();
2061 $ret = $this->manager
->beginAccountCreation( $creator, [ $userReq ], 'http://localhost/' );
2062 $this->unhook( 'LocalUserCreated' );
2063 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2064 $this->assertSame( 'userexists', $ret->message
->getKey() );
2066 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
2067 $mock->method( 'getUniqueId' )->willReturn( 'X' );
2068 $mock->method( 'accountCreationType' )
2069 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2070 $mock->method( 'testUserExists' )->willReturn( false );
2071 $mock->method( 'testUserForCreation' )
2072 ->willReturn( StatusValue
::newGood() );
2073 $mock->method( 'testForAccountCreation' )
2074 ->willReturn( StatusValue
::newFatal( 'fail' ) );
2075 $this->primaryauthMocks
= [ $mock ];
2076 $this->initializeManager( true );
2078 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
2079 ->onlyMethods( [ 'populateUser' ] )
2081 $req->method( 'populateUser' )
2082 ->willReturn( StatusValue
::newFatal( 'populatefail' ) );
2083 $userReq->username
= self
::usernameForCreation();
2084 $ret = $this->manager
->beginAccountCreation(
2085 $creator, [ $userReq, $req ], 'http://localhost/'
2087 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2088 $this->assertSame( 'populatefail', $ret->message
->getKey() );
2090 $req = new UserDataAuthenticationRequest
;
2091 $userReq->username
= self
::usernameForCreation();
2093 $ret = $this->manager
->beginAccountCreation(
2094 $creator, [ $userReq, $req ], 'http://localhost/'
2096 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2097 $this->assertSame( 'fail', $ret->message
->getKey() );
2099 $this->manager
->beginAccountCreation(
2100 User
::newFromName( $userReq->username
), [ $userReq, $req ], 'http://localhost/'
2102 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2103 $this->assertSame( 'fail', $ret->message
->getKey() );
2106 public function testContinueAccountCreation() {
2107 $creator = $this->getTestSysop()->getUser();
2108 $username = self
::usernameForCreation();
2109 $this->logger
= new TestLogger( false, static function ( $message, $level ) {
2110 return $level === LogLevel
::DEBUG ?
null : $message;
2112 $this->initializeManager();
2116 'username' => $username,
2118 'creatorname' => $username,
2120 'providerIds' => [ 'preauth' => [], 'primaryauth' => [], 'secondaryauth' => [] ],
2122 'primaryResponse' => null,
2124 'ranPreTests' => true,
2127 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2129 $this->manager
->continueAccountCreation( [] );
2130 $this->fail( 'Expected exception not thrown' );
2131 } catch ( LogicException
$ex ) {
2132 $this->assertEquals( 'Account creation is not possible', $ex->getMessage() );
2134 $this->unhook( 'LocalUserCreated' );
2136 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
2137 $mock->method( 'getUniqueId' )->willReturn( 'X' );
2138 $mock->method( 'accountCreationType' )
2139 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2140 $mock->method( 'testUserExists' )->willReturn( false );
2141 $mock->method( 'beginPrimaryAccountCreation' )
2142 ->willReturn( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) );
2143 $this->primaryauthMocks
= [ $mock ];
2144 $session['providerIds']['primaryauth'][] = 'X';
2145 $this->initializeManager( true );
2147 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
, null );
2148 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2149 $ret = $this->manager
->continueAccountCreation( [] );
2150 $this->unhook( 'LocalUserCreated' );
2151 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2152 $this->assertSame( 'authmanager-create-not-in-progress', $ret->message
->getKey() );
2154 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2155 [ 'username' => "$username<>" ] +
$session );
2156 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2157 $ret = $this->manager
->continueAccountCreation( [] );
2158 $this->unhook( 'LocalUserCreated' );
2159 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2160 $this->assertSame( 'noname', $ret->message
->getKey() );
2162 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2165 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
, $session );
2166 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2167 $cache = $this->objectCacheFactory
->getLocalClusterInstance();
2168 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
2169 $this->assertNotNull( $lock );
2170 $ret = $this->manager
->continueAccountCreation( [] );
2172 $this->unhook( 'LocalUserCreated' );
2173 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2174 $this->assertSame( 'usernameinprogress', $ret->message
->getKey() );
2175 // This error shouldn't remove the existing session, because the
2176 // raced-with process "owns" it.
2178 $session, $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2181 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2182 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2183 [ 'username' => $creator->getName() ] +
$session );
2184 $readOnlyMode = $this->getServiceContainer()->getReadOnlyMode();
2185 $readOnlyMode->setReason( 'Because' );
2186 $ret = $this->manager
->continueAccountCreation( [] );
2187 $this->unhook( 'LocalUserCreated' );
2188 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2189 $this->assertSame( 'readonlytext', $ret->message
->getKey() );
2190 $this->assertSame( [ 'Because' ], $ret->message
->getParams() );
2191 $readOnlyMode->setReason( false );
2193 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2194 [ 'username' => $creator->getName() ] +
$session );
2195 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2196 $ret = $this->manager
->continueAccountCreation( [] );
2197 $this->unhook( 'LocalUserCreated' );
2198 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2199 $this->assertSame( 'userexists', $ret->message
->getKey() );
2201 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2204 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2205 [ 'userid' => $creator->getId() ] +
$session );
2206 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2208 $ret = $this->manager
->continueAccountCreation( [] );
2209 $this->fail( 'Expected exception not thrown' );
2210 } catch ( UnexpectedValueException
$ex ) {
2211 $this->assertEquals( "User \"{$username}\" should exist now, but doesn't!", $ex->getMessage() );
2213 $this->unhook( 'LocalUserCreated' );
2215 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2218 $id = $creator->getId();
2219 $name = $creator->getName();
2220 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2221 [ 'username' => $name, 'userid' => $id +
1 ] +
$session );
2222 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2224 $ret = $this->manager
->continueAccountCreation( [] );
2225 $this->fail( 'Expected exception not thrown' );
2226 } catch ( UnexpectedValueException
$ex ) {
2227 $this->assertEquals(
2228 "User \"{$name}\" exists, but ID $id !== " . ( $id +
1 ) . '!', $ex->getMessage()
2231 $this->unhook( 'LocalUserCreated' );
2233 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2236 $req = $this->getMockBuilder( UserDataAuthenticationRequest
::class )
2237 ->onlyMethods( [ 'populateUser' ] )
2239 $req->method( 'populateUser' )
2240 ->willReturn( StatusValue
::newFatal( 'populatefail' ) );
2241 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
,
2242 [ 'reqs' => [ $req ] ] +
$session );
2243 $ret = $this->manager
->continueAccountCreation( [] );
2244 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
2245 $this->assertSame( 'populatefail', $ret->message
->getKey() );
2247 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
)
2252 * @dataProvider provideAccountCreation
2253 * @param StatusValue $preTest
2254 * @param StatusValue $primaryTest
2255 * @param StatusValue $secondaryTest
2256 * @param array $primaryResponses
2257 * @param array $secondaryResponses
2258 * @param array $managerResponses
2260 public function testAccountCreation(
2261 StatusValue
$preTest, $primaryTest, $secondaryTest,
2262 array $primaryResponses, array $secondaryResponses, array $managerResponses
2264 $creator = $this->getTestSysop()->getUser();
2265 $username = self
::usernameForCreation();
2267 $this->initializeManager();
2269 // Set up lots of mocks...
2270 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2272 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2273 $class = ucfirst( $key ) . 'AuthenticationProvider';
2274 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\Abstract$class" )
2275 ->setMockClassName( "MockAbstract$class" )
2277 $mocks[$key]->method( 'getUniqueId' )
2278 ->willReturn( $key );
2279 $mocks[$key]->method( 'testUserForCreation' )
2280 ->willReturn( StatusValue
::newGood() );
2281 $mocks[$key]->method( 'testForAccountCreation' )
2282 ->willReturnCallback(
2283 function ( $user, $creatorIn, $reqs )
2284 use ( $username, $creator, $req, $key, $preTest, $primaryTest, $secondaryTest )
2286 $this->assertSame( $username, $user->getName() );
2287 $this->assertSame( $creator->getId(), $creatorIn->getId() );
2288 $this->assertSame( $creator->getName(), $creatorIn->getName() );
2290 foreach ( $reqs as $r ) {
2291 $this->assertSame( $username, $r->username
);
2292 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
2294 $this->assertTrue( $foundReq, '$reqs contains $req' );
2295 if ( $key === 'pre' ) {
2298 if ( $key === 'primary' ) {
2299 return $primaryTest;
2301 return $secondaryTest;
2305 for ( $i = 2; $i <= 3; $i++
) {
2306 $mocks[$key . $i] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
2307 $mocks[$key . $i]->method( 'getUniqueId' )
2308 ->willReturn( $key . $i );
2309 $mocks[$key . $i]->method( 'testUserForCreation' )
2310 ->willReturn( StatusValue
::newGood() );
2311 $mocks[$key . $i]->expects( $this->atMost( 1 ) )->method( 'testForAccountCreation' )
2312 ->willReturn( StatusValue
::newGood() );
2316 $mocks['primary']->method( 'accountCreationType' )
2317 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2318 $mocks['primary']->method( 'testUserExists' )
2319 ->willReturn( false );
2320 $ct = count( $primaryResponses );
2321 $callback = $this->returnCallback( function ( $user, $creatorArg, $reqs ) use ( $creator, $username, $req, &$primaryResponses ) {
2322 $this->assertSame( $username, $user->getName() );
2323 $this->assertSame( $creator->getName(), $creatorArg->getName() );
2325 foreach ( $reqs as $r ) {
2326 $this->assertSame( $username, $r->username
);
2327 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
2329 $this->assertTrue( $foundReq, '$reqs contains $req' );
2330 return array_shift( $primaryResponses );
2332 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
2333 ->method( 'beginPrimaryAccountCreation' )
2334 ->will( $callback );
2335 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
2336 ->method( 'continuePrimaryAccountCreation' )
2337 ->will( $callback );
2339 $ct = count( $secondaryResponses );
2340 $callback = $this->returnCallback( function ( $user, $creatorArg, $reqs ) use ( $creator, $username, $req, &$secondaryResponses ) {
2341 $this->assertSame( $username, $user->getName() );
2342 $this->assertSame( $creator->getName(), $creatorArg->getName() );
2344 foreach ( $reqs as $r ) {
2345 $this->assertSame( $username, $r->username
);
2346 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
2348 $this->assertTrue( $foundReq, '$reqs contains $req' );
2349 return array_shift( $secondaryResponses );
2351 $mocks['secondary']->expects( $this->exactly( min( 1, $ct ) ) )
2352 ->method( 'beginSecondaryAccountCreation' )
2353 ->will( $callback );
2354 $mocks['secondary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
2355 ->method( 'continueSecondaryAccountCreation' )
2356 ->will( $callback );
2358 $abstain = AuthenticationResponse
::newAbstain();
2359 $mocks['primary2']->method( 'accountCreationType' )
2360 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
2361 $mocks['primary2']->method( 'testUserExists' )
2362 ->willReturn( false );
2363 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountCreation' )
2364 ->willReturn( $abstain );
2365 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
2366 $mocks['primary3']->method( 'accountCreationType' )
2367 ->willReturn( PrimaryAuthenticationProvider
::TYPE_NONE
);
2368 $mocks['primary3']->method( 'testUserExists' )
2369 ->willReturn( false );
2370 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountCreation' );
2371 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountCreation' );
2372 $mocks['secondary2']->expects( $this->atMost( 1 ) )
2373 ->method( 'beginSecondaryAccountCreation' )
2374 ->willReturn( $abstain );
2375 $mocks['secondary2']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
2376 $mocks['secondary3']->expects( $this->atMost( 1 ) )
2377 ->method( 'beginSecondaryAccountCreation' )
2378 ->willReturn( $abstain );
2379 $mocks['secondary3']->expects( $this->never() )->method( 'continueSecondaryAccountCreation' );
2381 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
2382 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary'], $mocks['primary2'] ];
2383 $this->secondaryauthMocks
= [
2384 $mocks['secondary3'], $mocks['secondary'], $mocks['secondary2']
2387 $this->logger
= new TestLogger( true, static function ( $message, $level ) {
2388 return $level === LogLevel
::DEBUG ?
null : $message;
2391 $this->initializeManager( true );
2393 $constraint = Assert
::logicalOr(
2394 $this->equalTo( AuthenticationResponse
::PASS
),
2395 $this->equalTo( AuthenticationResponse
::FAIL
)
2397 $providers = array_merge(
2398 $this->preauthMocks
, $this->primaryauthMocks
, $this->secondaryauthMocks
2400 foreach ( $providers as $p ) {
2401 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', false );
2402 $p->expects( $this->atMost( 1 ) )->method( 'postAccountCreation' )
2403 ->willReturnCallback( function ( $user, $creatorArg, $response )
2404 use ( $creator, $constraint, $p, $username )
2406 $this->assertInstanceOf( User
::class, $user );
2407 $this->assertSame( $username, $user->getName() );
2408 $this->assertSame( $creator->getName(), $creatorArg->getName() );
2409 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
2410 $this->assertThat( $response->status
, $constraint );
2411 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', $response->status
);
2415 // We're testing with $wgNewUserLog = false, so assert that it worked
2416 $dbw = $this->getDb();
2417 $maxLogId = $dbw->newSelectQueryBuilder()
2418 ->select( 'MAX(log_id)' )
2420 ->where( [ 'log_type' => 'newusers' ] )
2425 foreach ( $managerResponses as $i => $response ) {
2426 $success = $response instanceof AuthenticationResponse
&&
2427 $response->status
=== AuthenticationResponse
::PASS
;
2428 if ( $i === 'created' ) {
2430 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->once() )
2432 $this->callback( static function ( $user ) use ( $username ) {
2433 return $user->getName() === $username;
2437 $expectLog[] = [ LogLevel
::INFO
, "Creating user {user} during account creation" ];
2439 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2444 $userReq = new UsernameAuthenticationRequest
;
2445 $userReq->username
= $username;
2446 $ret = $this->manager
->beginAccountCreation(
2447 $creator, [ $userReq, $req ], 'http://localhost/'
2450 $ret = $this->manager
->continueAccountCreation( [ $req ] );
2452 if ( $response instanceof Exception
) {
2453 $this->fail( 'Expected exception not thrown', "Response $i" );
2455 } catch ( Exception
$ex ) {
2456 if ( !$response instanceof Exception
) {
2459 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2461 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
),
2462 "Response $i, exception, session state"
2464 $this->unhook( 'LocalUserCreated' );
2468 $this->unhook( 'LocalUserCreated' );
2470 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
2473 $this->assertNotNull( $ret->loginRequest
, "Response $i, login marker" );
2474 $this->assertContains(
2475 $ret->loginRequest
, $this->managerPriv
->createdAccountAuthenticationRequests
,
2476 "Response $i, login marker"
2481 "MediaWiki\Auth\AuthManager::continueAccountCreation: Account creation succeeded for {user}"
2484 // Set some fields in the expected $response that we couldn't
2485 // know in provideAccountCreation().
2486 $response->username
= $username;
2487 $response->loginRequest
= $ret->loginRequest
;
2489 $this->assertNull( $ret->loginRequest
, "Response $i, login marker" );
2490 $this->assertSame( [], $this->managerPriv
->createdAccountAuthenticationRequests
,
2491 "Response $i, login marker" );
2493 $ret->message
= $this->message( $ret->message
);
2494 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
2495 if ( $success ||
$response->status
=== AuthenticationResponse
::FAIL
) {
2497 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
),
2498 "Response $i, session state"
2500 foreach ( $providers as $p ) {
2501 $this->assertSame( $response->status
, DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ),
2502 "Response $i, post-auth callback called" );
2505 $this->assertNotNull(
2506 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_CREATION_STATE
),
2507 "Response $i, session state"
2509 foreach ( $ret->neededRequests
as $neededReq ) {
2510 $this->assertEquals( AuthManager
::ACTION_CREATE
, $neededReq->action
,
2511 "Response $i, neededRequest action" );
2513 $this->assertEquals(
2514 $ret->neededRequests
,
2515 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_CREATE_CONTINUE
),
2516 "Response $i, continuation check"
2518 foreach ( $providers as $p ) {
2519 $this->assertFalse( DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ), "Response $i, post-auth callback not called" );
2523 $userIdentity = $this->userIdentityLookup
->getUserIdentityByName( $username );
2524 $this->assertSame( $created, $userIdentity && $userIdentity->isRegistered() );
2529 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
2533 $dbw->newSelectQueryBuilder()
2534 ->select( 'MAX(log_id)' )
2536 ->where( [ 'log_type' => 'newusers' ] )
2540 public function provideAccountCreation() {
2541 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
2542 $good = StatusValue
::newGood();
2545 'Pre-creation test fail in pre' => [
2546 StatusValue
::newFatal( 'fail-from-pre' ), $good, $good,
2550 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
2553 'Pre-creation test fail in primary' => [
2554 $good, StatusValue
::newFatal( 'fail-from-primary' ), $good,
2558 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2561 'Pre-creation test fail in secondary' => [
2562 $good, $good, StatusValue
::newFatal( 'fail-from-secondary' ),
2566 AuthenticationResponse
::newFail( $this->message( 'fail-from-secondary' ) ),
2569 'Failure in primary' => [
2570 $good, $good, $good,
2572 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
2577 'All primary abstain' => [
2578 $good, $good, $good,
2580 AuthenticationResponse
::newAbstain(),
2584 AuthenticationResponse
::newFail( $this->message( 'authmanager-create-no-primary' ) )
2587 'Primary UI, then redirect, then fail' => [
2588 $good, $good, $good,
2590 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2591 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2592 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
2597 'Primary redirect, then abstain' => [
2598 $good, $good, $good,
2600 $tmp = AuthenticationResponse
::newRedirect(
2601 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2603 AuthenticationResponse
::newAbstain(),
2608 new DomainException(
2609 'MockAbstractPrimaryAuthenticationProvider::continuePrimaryAccountCreation() returned ABSTAIN'
2613 'Primary UI, then pass; secondary abstain' => [
2614 $good, $good, $good,
2616 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2617 AuthenticationResponse
::newPass(),
2620 AuthenticationResponse
::newAbstain(),
2624 'created' => AuthenticationResponse
::newPass( '' ),
2627 'Primary pass; secondary UI then pass' => [
2628 $good, $good, $good,
2630 AuthenticationResponse
::newPass( '' ),
2633 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
2634 AuthenticationResponse
::newPass( '' ),
2638 AuthenticationResponse
::newPass( '' ),
2641 'Primary pass; secondary fail' => [
2642 $good, $good, $good,
2644 AuthenticationResponse
::newPass(),
2647 AuthenticationResponse
::newFail( $this->message( '...' ) ),
2650 'created' => new DomainException(
2651 'MockAbstractSecondaryAuthenticationProvider::beginSecondaryAccountCreation() returned FAIL. ' .
2652 'Secondary providers are not allowed to fail account creation, ' .
2653 'that should have been done via testForAccountCreation().'
2661 * @dataProvider provideAccountCreationLogging
2662 * @param bool $isAnon
2663 * @param string|null $logSubtype
2665 public function testAccountCreationLogging( $isAnon, $logSubtype ) {
2666 $creator = $isAnon ?
new User
: $this->getTestSysop()->getUser();
2667 $username = self
::usernameForCreation();
2669 $this->initializeManager();
2671 // Set up lots of mocks...
2672 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
2673 $mock->method( 'getUniqueId' )
2674 ->willReturn( 'primary' );
2675 $mock->method( 'testUserForCreation' )
2676 ->willReturn( StatusValue
::newGood() );
2677 $mock->method( 'testForAccountCreation' )
2678 ->willReturn( StatusValue
::newGood() );
2679 $mock->method( 'accountCreationType' )
2680 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2681 $mock->method( 'testUserExists' )
2682 ->willReturn( false );
2683 $mock->method( 'beginPrimaryAccountCreation' )
2684 ->willReturn( AuthenticationResponse
::newPass( $username ) );
2685 $mock->method( 'finishAccountCreation' )
2686 ->willReturn( $logSubtype );
2688 $this->primaryauthMocks
= [ $mock ];
2689 $this->initializeManager( true );
2690 $this->logger
->setCollect( true );
2692 $this->config
->set( MainConfigNames
::NewUserLog
, true );
2694 $dbw = $this->getDb();
2695 $maxLogId = $dbw->newSelectQueryBuilder()
2696 ->select( 'MAX(log_id)' )
2698 ->where( [ 'log_type' => 'newusers' ] )
2701 $userReq = new UsernameAuthenticationRequest
;
2702 $userReq->username
= $username;
2703 $reasonReq = new CreationReasonAuthenticationRequest
;
2704 $reasonReq->reason
= $this->toString();
2705 $ret = $this->manager
->beginAccountCreation(
2706 $creator, [ $userReq, $reasonReq ], 'http://localhost/'
2709 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
2711 $user = User
::newFromName( $username );
2712 $this->assertNotEquals( 0, $user->getId() );
2713 $this->assertNotEquals( $creator->getId(), $user->getId() );
2715 $queryBuilder = DatabaseLogEntry
::newSelectQueryBuilder( $dbw )
2716 ->where( [ 'log_id > ' . (int)$maxLogId, 'log_type' => 'newusers' ] );
2717 $rows = iterator_to_array( $queryBuilder->caller( __METHOD__
)->fetchResultSet() );
2718 $this->assertCount( 1, $rows );
2719 $entry = DatabaseLogEntry
::newFromRow( reset( $rows ) );
2721 $this->assertSame( $logSubtype ?
: ( $isAnon ?
'create' : 'create2' ), $entry->getSubtype() );
2723 $isAnon ?
$user->getId() : $creator->getId(),
2724 $entry->getPerformerIdentity()->getId()
2727 $isAnon ?
$user->getName() : $creator->getName(),
2728 $entry->getPerformerIdentity()->getName()
2730 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
2731 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
2732 $this->assertSame( $this->toString(), $entry->getComment() );
2735 public static function provideAccountCreationLogging() {
2740 [ false, 'byemail' ],
2744 public function testAccountCreation_AuthManagerVerifyAuthentication() {
2745 $this->logger
= new NullLogger();
2746 $this->initializeManager();
2749 'getUniqueId' => 'primary',
2750 'accountCreationType' => PrimaryAuthenticationProvider
::TYPE_CREATE
,
2751 'testUserForCreation' => StatusValue
::newGood(),
2752 'testForAccountCreation' => StatusValue
::newGood(),
2753 'getAuthenticationRequests' => [],
2754 'beginPrimaryAccountCreation' => AuthenticationResponse
::newPass(),
2756 $secondaryConfig = [
2757 'getUniqueId' => 'secondary',
2758 'testUserForCreation' => StatusValue
::newGood(),
2759 'testForAccountCreation' => StatusValue
::newGood(),
2760 'getAuthenticationRequests' => [],
2761 'beginSecondaryAccountCreation' => AuthenticationResponse
::newAbstain(),
2763 $updateManager = function () use ( &$primaryConfig, &$secondaryConfig ) {
2764 $primaryMock = $this->createConfiguredMock( AbstractPrimaryAuthenticationProvider
::class, $primaryConfig );
2765 foreach ( [ 'beginPrimaryAccountCreation', 'continuePrimaryAccountCreation' ] as $method ) {
2766 $primaryMock->expects(
2767 array_key_exists( $method, $primaryConfig ) ?
$this->once() : $this->never()
2768 )->method( $method );
2770 $secondaryMock = $this->createConfiguredMock( AbstractSecondaryAuthenticationProvider
::class, $secondaryConfig );
2771 foreach ( [ 'beginSecondaryAccountCreation', 'continueSecondaryAccountCreation' ] as $method ) {
2772 $secondaryMock->expects(
2773 array_key_exists( $method, $secondaryConfig ) ?
$this->once() : $this->never()
2774 )->method( $method );
2776 $this->primaryauthMocks
= [ $primaryMock ];
2777 $this->secondaryauthMocks
= [ $secondaryMock ];
2778 $this->initializeManager( true );
2780 $req = new UsernameAuthenticationRequest();
2781 $req->username
= 'UTDummy';
2783 // Gets expected data
2785 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
2786 $hook->willReturnCallback( function ( $user, &$response, $authManager, $info ) {
2787 $this->assertSame( 'UTDummy', $user->getName() );
2788 $this->assertSame( AuthenticationResponse
::PASS
, $response->status
);
2789 $this->assertSame( $this->manager
, $authManager );
2790 $this->assertSame( AuthManager
::ACTION_CREATE
, $info['action'] );
2791 $this->assertSame( 'primary', $info['primaryId'] );
2793 $response = $this->manager
->beginAccountCreation( new User(), [ $req ], 'http://localhost/' );
2794 // Simplify verifying $response, loginRequest would include the user ID
2795 $response->loginRequest
= null;
2796 $this->assertEquals( AuthenticationResponse
::newPass( 'UTDummy' ), $response );
2797 $this->assertNotNull( $this->manager
->getRequest()->getSession()->getUser() );
2798 $this->assertTrue( $this->getServiceContainer()->getUserFactory()->newFromName( 'UTDummy' )->isRegistered() );
2799 $this->unhook( 'AuthManagerVerifyAuthentication' );
2801 // Will prevent login
2802 unset( $secondaryConfig['beginSecondaryAccountCreation'] );
2804 $req = new UsernameAuthenticationRequest();
2805 $req->username
= 'UTDummy2';
2806 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook
::class, $this->once() );
2807 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
2808 $response = AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) );
2811 $response = $this->manager
->beginAccountCreation( new User(), [ $req ], 'http://localhost/' );
2812 $this->assertEquals( AuthenticationResponse
::newFail( wfMessage( 'hook-fail' ) ), $response );
2813 $this->assertFalse( $this->getServiceContainer()->getUserFactory()->newFromName( 'UTDummy2' )->isRegistered() );
2814 $this->unhook( 'AuthManagerVerifyAuthentication' );
2816 // the LogicError paths are already tested under testAuthentication_AuthManagerVerifyAuthentication
2819 public function testAutoAccountCreation() {
2820 // PHPUnit seems to have a bug where it will call the ->with()
2821 // callbacks for our hooks again after the test is run (WTF?), which
2822 // breaks here because $username no longer matches $user by the end of
2824 $workaroundPHPUnitBug = false;
2826 $username = self
::usernameForCreation();
2827 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
2829 $this->setGroupPermissions( [
2831 'createaccount' => true,
2832 'autocreateaccount' => false,
2835 $this->initializeManager( true );
2837 // Set up lots of mocks...
2839 foreach ( [ 'pre', 'primary', 'secondary' ] as $key ) {
2840 $class = ucfirst( $key ) . 'AuthenticationProvider';
2841 $mocks[$key] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
2842 $mocks[$key]->method( 'getUniqueId' )
2843 ->willReturn( $key );
2846 $good = StatusValue
::newGood();
2847 $ok = StatusValue
::newFatal( 'ok' );
2848 $callback = $this->callback( static function ( $user ) use ( &$username, &$workaroundPHPUnitBug ) {
2849 return $workaroundPHPUnitBug ||
$user->getName() === $username;
2851 $callback2 = $this->callback(
2852 static function ( $source ) use ( &$expectedSource, &$workaroundPHPUnitBug ) {
2853 return $workaroundPHPUnitBug ||
$source === $expectedSource;
2857 $mocks['pre']->expects( $this->exactly( 13 ) )->method( 'testUserForCreation' )
2858 ->with( $callback, $callback2 )
2859 ->willReturnOnConsecutiveCalls(
2860 $ok, $ok, $ok, // For testing permissions
2861 StatusValue
::newFatal( 'fail-in-pre' ), $good, $good,
2862 $good, // backoff test
2863 $good, // addToDatabase fails test
2864 $good, // addToDatabase throws test
2865 $good, // addToDatabase exists test
2866 $good, $good, $good // success
2869 $mocks['primary']->method( 'accountCreationType' )
2870 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
2871 $mocks['primary']->method( 'testUserExists' )
2872 ->willReturn( true );
2873 $mocks['primary']->expects( $this->exactly( 9 ) )->method( 'testUserForCreation' )
2874 ->with( $callback, $callback2 )
2875 ->willReturnOnConsecutiveCalls(
2876 StatusValue
::newFatal( 'fail-in-primary' ), $good,
2877 $good, // backoff test
2878 $good, // addToDatabase fails test
2879 $good, // addToDatabase throws test
2880 $good, // addToDatabase exists test
2883 $mocks['primary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2884 ->with( $callback, $callback2 );
2886 $mocks['secondary']->expects( $this->exactly( 8 ) )->method( 'testUserForCreation' )
2887 ->with( $callback, $callback2 )
2888 ->willReturnOnConsecutiveCalls(
2889 StatusValue
::newFatal( 'fail-in-secondary' ),
2890 $good, // backoff test
2891 $good, // addToDatabase fails test
2892 $good, // addToDatabase throws test
2893 $good, // addToDatabase exists test
2896 $mocks['secondary']->expects( $this->exactly( 3 ) )->method( 'autoCreatedAccount' )
2897 ->with( $callback, $callback2 );
2899 $this->preauthMocks
= [ $mocks['pre'] ];
2900 $this->primaryauthMocks
= [ $mocks['primary'] ];
2901 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
2902 $this->initializeManager( true );
2903 $session = $this->request
->getSession();
2905 $logger = new TestLogger( true, static function ( $m ) {
2906 $m = str_replace( 'MediaWiki\\Auth\\AuthManager::autoCreateUser: ', '', $m );
2909 $this->logger
= $logger;
2910 $this->manager
->setLogger( $logger );
2913 $userMock = $this->createMock( User
::class );
2914 $this->manager
->autoCreateUser( $userMock, 'InvalidSource', true, true );
2915 $this->fail( 'Expected exception not thrown' );
2916 } catch ( InvalidArgumentException
$ex ) {
2917 $this->assertSame( 'Unknown auto-creation source: InvalidSource', $ex->getMessage() );
2920 // First, check an existing user
2922 $existingUser = $this->getTestSysop()->getUser();
2923 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2924 $ret = $this->manager
->autoCreateUser( $existingUser, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
2925 $this->unhook( 'LocalUserCreated' );
2926 $expect = Status
::newGood();
2927 $expect->warning( 'userexists' );
2928 $this->assertEquals( $expect, $ret );
2929 $this->assertNotEquals( 0, $existingUser->getId() );
2930 $this->assertEquals( $existingUser->getId(), $session->getUser()->getId() );
2931 $this->assertSame( [
2932 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2933 ], $logger->getBuffer() );
2934 $logger->clearBuffer();
2937 $user = $this->getTestSysop()->getUser();
2938 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2939 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false, true );
2940 $this->unhook( 'LocalUserCreated' );
2941 $expect = Status
::newGood();
2942 $expect->warning( 'userexists' );
2943 $this->assertEquals( $expect, $ret );
2944 $this->assertNotEquals( 0, $user->getId() );
2945 $this->assertSame( 0, $session->getUser()->getId() );
2946 $this->assertSame( [
2947 [ LogLevel
::DEBUG
, '{username} already exists locally' ],
2948 ], $logger->getBuffer() );
2949 $logger->clearBuffer();
2951 // Wiki is read-only
2953 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2954 $readOnlyMode = $this->getServiceContainer()->getReadOnlyMode();
2955 $readOnlyMode->setReason( 'Because' );
2956 $user = User
::newFromName( $username );
2957 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
2958 $this->unhook( 'LocalUserCreated' );
2959 $this->assertEquals( Status
::newFatal( wfMessage( 'readonlytext', 'Because' ) ), $ret );
2960 $this->assertSame( 0, $user->getId() );
2961 $this->assertNotEquals( $username, $user->getName() );
2962 $this->assertSame( 0, $session->getUser()->getId() );
2963 $this->assertSame( [
2964 [ LogLevel
::DEBUG
, 'denied because of read only mode: {reason}' ],
2965 ], $logger->getBuffer() );
2966 $logger->clearBuffer();
2967 $readOnlyMode->setReason( false );
2969 // Session blacklisted
2971 $session->set( AuthManager
::AUTOCREATE_BLOCKLIST
, 'test' );
2972 $user = User
::newFromName( $username );
2973 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2974 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
2975 $this->unhook( 'LocalUserCreated' );
2976 $this->assertEquals( Status
::newFatal( 'test' ), $ret );
2977 $this->assertSame( 0, $user->getId() );
2978 $this->assertNotEquals( $username, $user->getName() );
2979 $this->assertSame( 0, $session->getUser()->getId() );
2980 $this->assertSame( [
2981 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2982 ], $logger->getBuffer() );
2983 $logger->clearBuffer();
2986 $session->set( AuthManager
::AUTOCREATE_BLOCKLIST
, StatusValue
::newFatal( 'test2' ) );
2987 $user = User
::newFromName( $username );
2988 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
2989 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
2990 $this->unhook( 'LocalUserCreated' );
2991 $this->assertEquals( Status
::newFatal( 'test2' ), $ret );
2992 $this->assertSame( 0, $user->getId() );
2993 $this->assertNotEquals( $username, $user->getName() );
2994 $this->assertSame( 0, $session->getUser()->getId() );
2995 $this->assertSame( [
2996 [ LogLevel
::DEBUG
, 'blacklisted in session {sessionid}' ],
2997 ], $logger->getBuffer() );
2998 $logger->clearBuffer();
3002 $user = User
::newFromName( $username . "\u{0080}", false );
3003 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3004 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3005 $this->unhook( 'LocalUserCreated' );
3006 $this->assertEquals( Status
::newFatal( 'noname' ), $ret );
3007 $this->assertSame( 0, $user->getId() );
3008 $this->assertNotEquals( $username . "\u{0080}", $user->getId() );
3009 $this->assertSame( 0, $session->getUser()->getId() );
3010 $this->assertSame( [
3011 [ LogLevel
::DEBUG
, 'name "{username}" is not usable' ],
3012 ], $logger->getBuffer() );
3013 $logger->clearBuffer();
3014 $this->assertSame( 'noname', $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
) );
3016 // IP unable to create accounts
3017 $this->setGroupPermissions( [
3019 'createaccount' => false,
3020 'autocreateaccount' => false,
3023 $this->initializeManager( true );
3024 $session = $this->request
->getSession();
3025 $user = User
::newFromName( $username );
3026 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3027 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3028 $this->unhook( 'LocalUserCreated' );
3029 $this->assertTrue( $ret->hasMessage( 'badaccess-group0' ) );
3030 $this->assertSame( 0, $user->getId() );
3031 $this->assertNotEquals( $username, $user->getName() );
3032 $this->assertSame( 0, $session->getUser()->getId() );
3033 $this->assertSame( [
3034 [ LogLevel
::DEBUG
, 'cannot create or autocreate accounts' ],
3035 ], $logger->getBuffer() );
3036 $logger->clearBuffer();
3037 $this->assertEquals(
3038 (string)$ret, (string)$session->get( AuthManager
::AUTOCREATE_BLOCKLIST
)
3041 // maintenance scripts always work
3042 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_MAINT
;
3043 $this->initializeManager( true );
3044 $session = $this->request
->getSession();
3045 $user = User
::newFromName( $username );
3046 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3047 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_MAINT
, true, false );
3048 $this->unhook( 'LocalUserCreated' );
3049 $this->assertStatusError( 'ok', $ret );
3051 // Test that both permutations of permissions are allowed
3052 // (this hits the two "ok" entries in $mocks['pre'])
3053 $expectedSource = AuthManager
::AUTOCREATE_SOURCE_SESSION
;
3054 $this->setGroupPermissions( '*', 'autocreateaccount', true );
3055 $this->initializeManager( true );
3056 $session = $this->request
->getSession();
3057 $user = User
::newFromName( $username );
3058 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3059 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3060 $this->unhook( 'LocalUserCreated' );
3061 $this->assertStatusError( 'ok', $ret );
3063 $this->setGroupPermissions( [
3065 'createaccount' => true,
3066 'autocreateaccount' => false,
3069 $this->initializeManager( true );
3070 $session = $this->request
->getSession();
3071 $user = User
::newFromName( $username );
3072 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3073 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3074 $this->unhook( 'LocalUserCreated' );
3075 $this->assertStatusError( 'ok', $ret );
3076 $logger->clearBuffer();
3080 unset( $this->objectCacheFactory
);
3081 $this->initializeManager( true );
3082 $session = $this->request
->getSession();
3083 $user = User
::newFromName( $username );
3084 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3085 $cache = $this->objectCacheFactory
->getLocalClusterInstance();
3086 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
3087 $this->assertNotNull( $lock );
3088 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3090 $this->unhook( 'LocalUserCreated' );
3091 $this->assertStatusError( 'usernameinprogress', $ret );
3092 $this->assertSame( 0, $user->getId() );
3093 $this->assertNotEquals( $username, $user->getName() );
3094 $this->assertSame( 0, $session->getUser()->getId() );
3095 $this->assertSame( [
3096 [ LogLevel
::DEBUG
, 'Could not acquire account creation lock' ],
3097 ], $logger->getBuffer() );
3098 $logger->clearBuffer();
3100 // Test pre-authentication provider fail
3102 $user = User
::newFromName( $username );
3103 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3104 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3105 $this->unhook( 'LocalUserCreated' );
3106 $this->assertStatusError( 'fail-in-pre', $ret );
3107 $this->assertSame( 0, $user->getId() );
3108 $this->assertNotEquals( $username, $user->getName() );
3109 $this->assertSame( 0, $session->getUser()->getId() );
3110 $this->assertSame( [
3111 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
3112 ], $logger->getBuffer() );
3113 $logger->clearBuffer();
3114 $this->assertEquals(
3115 StatusValue
::newFatal( 'fail-in-pre' ), $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
)
3119 $user = User
::newFromName( $username );
3120 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3121 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3122 $this->unhook( 'LocalUserCreated' );
3123 $this->assertStatusError( 'fail-in-primary', $ret );
3124 $this->assertSame( 0, $user->getId() );
3125 $this->assertNotEquals( $username, $user->getName() );
3126 $this->assertSame( 0, $session->getUser()->getId() );
3127 $this->assertSame( [
3128 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
3129 ], $logger->getBuffer() );
3130 $logger->clearBuffer();
3131 $this->assertEquals(
3132 StatusValue
::newFatal( 'fail-in-primary' ), $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
)
3136 $user = User
::newFromName( $username );
3137 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3138 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3139 $this->unhook( 'LocalUserCreated' );
3140 $this->assertStatusError( 'fail-in-secondary', $ret );
3141 $this->assertSame( 0, $user->getId() );
3142 $this->assertNotEquals( $username, $user->getName() );
3143 $this->assertSame( 0, $session->getUser()->getId() );
3144 $this->assertSame( [
3145 [ LogLevel
::DEBUG
, 'Provider denied creation of {username}: {reason}' ],
3146 ], $logger->getBuffer() );
3147 $logger->clearBuffer();
3148 $this->assertEquals(
3149 StatusValue
::newFatal( 'fail-in-secondary' ), $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
)
3153 $cache = $this->objectCacheFactory
->getLocalClusterInstance();
3154 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
3155 $cache->set( $backoffKey, true );
3157 $user = User
::newFromName( $username );
3158 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3159 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3160 $this->unhook( 'LocalUserCreated' );
3161 $this->assertStatusError( 'authmanager-autocreate-exception', $ret );
3162 $this->assertSame( 0, $user->getId() );
3163 $this->assertNotEquals( $username, $user->getName() );
3164 $this->assertSame( 0, $session->getUser()->getId() );
3165 $this->assertSame( [
3166 [ LogLevel
::DEBUG
, '{username} denied by prior creation attempt failures' ],
3167 ], $logger->getBuffer() );
3168 $logger->clearBuffer();
3169 $this->assertSame( null, $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
) );
3170 $cache->delete( $backoffKey );
3172 // Test addToDatabase fails
3174 $user = $this->getMockBuilder( User
::class )
3175 ->onlyMethods( [ 'addToDatabase' ] )->getMock();
3176 $user->expects( $this->once() )->method( 'addToDatabase' )
3177 ->willReturn( Status
::newFatal( 'because' ) );
3178 $user->setName( $username );
3179 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3180 $this->assertStatusError( 'because', $ret );
3181 $this->assertSame( 0, $user->getId() );
3182 $this->assertNotEquals( $username, $user->getName() );
3183 $this->assertSame( 0, $session->getUser()->getId() );
3184 $this->assertSame( [
3185 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
3186 [ LogLevel
::ERROR
, '{username} failed with message {msg}' ],
3187 ], $logger->getBuffer() );
3188 $logger->clearBuffer();
3189 $this->assertSame( null, $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
) );
3191 // Test addToDatabase throws an exception
3192 $cache = $this->objectCacheFactory
->getLocalClusterInstance();
3193 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
3194 $this->assertFalse( $cache->get( $backoffKey ) );
3196 $user = $this->getMockBuilder( User
::class )
3197 ->onlyMethods( [ 'addToDatabase' ] )->getMock();
3198 $user->expects( $this->once() )->method( 'addToDatabase' )
3199 ->willThrowException( new Exception( 'Excepted' ) );
3200 $user->setName( $username );
3202 $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3203 $this->fail( 'Expected exception not thrown' );
3204 } catch ( Exception
$ex ) {
3205 $this->assertSame( 'Excepted', $ex->getMessage() );
3207 $this->assertSame( 0, $user->getId() );
3208 $this->assertSame( 0, $session->getUser()->getId() );
3209 $this->assertSame( [
3210 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
3211 [ LogLevel
::ERROR
, '{username} failed with exception {exception}' ],
3212 ], $logger->getBuffer() );
3213 $logger->clearBuffer();
3214 $this->assertSame( null, $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
) );
3215 $this->assertNotFalse( $cache->get( $backoffKey ) );
3216 $cache->delete( $backoffKey );
3218 // Test addToDatabase fails because the user already exists.
3220 $user = $this->getMockBuilder( User
::class )
3221 ->onlyMethods( [ 'addToDatabase' ] )->getMock();
3222 $user->expects( $this->once() )->method( 'addToDatabase' )
3223 ->willReturnCallback( function () use ( $username, &$user ) {
3224 $oldUser = User
::newFromName( $username );
3225 $status = $oldUser->addToDatabase();
3226 $this->assertStatusOK( $status );
3227 $user->setId( $oldUser->getId() );
3228 return Status
::newFatal( 'userexists' );
3230 $user->setName( $username );
3231 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3232 $expect = Status
::newGood();
3233 $expect->warning( 'userexists' );
3234 $this->assertEquals( $expect, $ret );
3235 $this->assertNotEquals( 0, $user->getId() );
3236 $this->assertEquals( $username, $user->getName() );
3237 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
3238 $this->assertSame( [
3239 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
3240 [ LogLevel
::INFO
, '{username} already exists locally (race)' ],
3241 ], $logger->getBuffer() );
3242 $logger->clearBuffer();
3243 $this->assertSame( null, $session->get( AuthManager
::AUTOCREATE_BLOCKLIST
) );
3247 $username = self
::usernameForCreation();
3248 $user = User
::newFromName( $username );
3249 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->once() )
3250 ->with( $callback, true );
3251 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true );
3252 $this->unhook( 'LocalUserCreated' );
3253 $this->assertEquals( Status
::newGood(), $ret );
3254 $this->assertNotEquals( 0, $user->getId() );
3255 $this->assertEquals( $username, $user->getName() );
3256 $this->assertEquals( $user->getId(), $session->getUser()->getId() );
3257 $this->assertSame( [
3258 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
3259 ], $logger->getBuffer() );
3260 $logger->clearBuffer();
3262 $dbw = $this->getDb();
3263 $maxLogId = $dbw->newSelectQueryBuilder()
3264 ->select( 'MAX(log_id)' )
3266 ->where( [ 'log_type' => 'newusers' ] )
3269 $username = self
::usernameForCreation();
3270 $user = User
::newFromName( $username );
3271 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->once() )
3272 ->with( $callback, true );
3273 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false, true );
3274 $this->unhook( 'LocalUserCreated' );
3275 $this->assertEquals( Status
::newGood(), $ret );
3276 $this->assertNotEquals( 0, $user->getId() );
3277 $this->assertEquals( $username, $user->getName() );
3278 $this->assertSame( 0, $session->getUser()->getId() );
3279 $this->assertSame( [
3280 [ LogLevel
::INFO
, 'creating new user ({username}) - from: {from}' ],
3281 ], $logger->getBuffer() );
3282 $logger->clearBuffer();
3285 $dbw->newSelectQueryBuilder()
3286 ->select( 'MAX(log_id)' )
3288 ->where( [ 'log_type' => 'newusers' ] )
3291 $this->config
->set( MainConfigNames
::NewUserLog
, true );
3293 $username = self
::usernameForCreation();
3294 $user = User
::newFromName( $username );
3295 $ret = $this->manager
->autoCreateUser( $user, AuthManager
::AUTOCREATE_SOURCE_SESSION
, false, true );
3296 $this->assertEquals( Status
::newGood(), $ret );
3297 $logger->clearBuffer();
3299 $queryBuilder = DatabaseLogEntry
::newSelectQueryBuilder( $dbw )
3300 ->where( [ 'log_id > ' . (int)$maxLogId, 'log_type' => 'newusers' ] );
3301 $rows = iterator_to_array( $queryBuilder->caller( __METHOD__
)->fetchResultSet() );
3302 $this->assertCount( 1, $rows );
3303 $entry = DatabaseLogEntry
::newFromRow( reset( $rows ) );
3305 $this->assertSame( 'autocreate', $entry->getSubtype() );
3306 $this->assertSame( $user->getId(), $entry->getPerformerIdentity()->getId() );
3307 $this->assertSame( $user->getName(), $entry->getPerformerIdentity()->getName() );
3308 $this->assertSame( $user->getUserPage()->getFullText(), $entry->getTarget()->getFullText() );
3309 $this->assertSame( [ '4::userid' => $user->getId() ], $entry->getParameters() );
3311 $workaroundPHPUnitBug = true;
3315 * @dataProvider provideAutoCreateUserBlocks
3317 public function testAutoCreateUserBlocks(
3319 array $blockOptions,
3320 string $performerType,
3321 bool $expectedStatus
3324 if ( $blockType === 'ip' ) {
3325 $blockOptions['address'] = '127.0.0.0/24';
3326 } elseif ( $blockType === 'global-ip' ) {
3327 $this->setTemporaryHook( 'GetUserBlock',
3328 static function ( $user, $ip, &$block ) use ( $blockOptions ) {
3329 $block = new SystemBlock( $blockOptions );
3330 $block->isCreateAccountBlocked( true );
3333 $blockOptions = null;
3334 } elseif ( $blockType === 'none' ) {
3335 $blockOptions = null;
3337 $this->fail( "Unknown block type \"$blockType\"" );
3340 if ( $blockOptions !== null ) {
3342 'by' => $this->getTestSysop()->getUser(),
3343 'reason' => __METHOD__
,
3344 'expiry' => time() +
100500,
3346 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
3347 $block = new DatabaseBlock( $blockOptions );
3348 $blockStore->insertBlock( $block );
3351 if ( $performerType === 'sysop' ) {
3352 $performer = $this->getTestSysop()->getUser();
3353 } elseif ( $performerType === 'anon' ) {
3356 $this->fail( "Unknown performer type \"$performerType\"" );
3359 $this->logger
= LoggerFactory
::getInstance( 'AuthManagerTest' );
3360 $this->initializeManager( true );
3362 $user = $this->userFactory
->newFromName( 'NewUser' );
3363 $status = $this->manager
->autoCreateUser( $user,
3364 AuthManager
::AUTOCREATE_SOURCE_SESSION
, true, true, $performer );
3365 $this->assertSame( $expectedStatus, $status->isGood() );
3368 public static function provideAutoCreateUserBlocks() {
3370 // block type (ip/global/none), block options, performer, expected status
3371 'not blocked' => [ 'none', [], 'anon', true ],
3372 'ip-blocked' => [ 'ip', [], 'anon', true ],
3373 'ip-blocked with createAccount' => [
3375 [ 'createAccount' => true ],
3379 'partially ip-blocked' => [
3381 [ 'restrictions' => [ new PageRestriction( 0, 1 ) ] ],
3385 'ip-blocked with sysop performer' => [
3387 [ 'createAccount' => true ],
3391 'globally blocked' => [
3393 [ 'systemBlock' => 'test-systemBlock' ],
3401 * @dataProvider provideGetAuthenticationRequests
3402 * @param string $action
3403 * @param array $expect
3404 * @param array $state
3406 public function testGetAuthenticationRequests( $action, $expect, $state = [] ) {
3407 $makeReq = function ( $key ) use ( $action ) {
3408 $req = $this->createMock( AuthenticationRequest
::class );
3409 $req->method( 'getUniqueId' )
3410 ->willReturn( $key );
3411 $req->action
= $action === AuthManager
::ACTION_UNLINK ? AuthManager
::ACTION_REMOVE
: $action;
3414 $cmpReqs = static function ( $a, $b ) {
3415 $ret = strcmp( get_class( $a ), get_class( $b ) );
3417 $ret = strcmp( $a->getUniqueId(), $b->getUniqueId() );
3422 $good = StatusValue
::newGood();
3425 $mocks['pre'] = $this->createMock( AbstractPreAuthenticationProvider
::class );
3426 $mocks['pre']->method( 'getUniqueId' )
3427 ->willReturn( 'pre' );
3428 $mocks['pre']->method( 'getAuthenticationRequests' )
3429 ->willReturnCallback( static function ( $action ) use ( $makeReq ) {
3430 return [ $makeReq( "pre-$action" ), $makeReq( 'generic' ) ];
3432 foreach ( [ 'primary', 'secondary' ] as $key ) {
3433 $class = ucfirst( $key ) . 'AuthenticationProvider';
3434 $mocks[$key] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
3435 $mocks[$key]->method( 'getUniqueId' )
3436 ->willReturn( $key );
3437 $mocks[$key]->method( 'getAuthenticationRequests' )
3438 ->willReturnCallback( static function ( $action ) use ( $key, $makeReq ) {
3439 return [ $makeReq( "$key-$action" ), $makeReq( 'generic' ) ];
3441 $mocks[$key]->method( 'providerAllowsAuthenticationDataChange' )
3442 ->willReturn( $good );
3446 PrimaryAuthenticationProvider
::TYPE_NONE
,
3447 PrimaryAuthenticationProvider
::TYPE_CREATE
,
3448 PrimaryAuthenticationProvider
::TYPE_LINK
3450 $class = 'PrimaryAuthenticationProvider';
3451 $mocks["primary-$type"] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
3452 $mocks["primary-$type"]->method( 'getUniqueId' )
3453 ->willReturn( "primary-$type" );
3454 $mocks["primary-$type"]->method( 'accountCreationType' )
3455 ->willReturn( $type );
3456 $mocks["primary-$type"]->method( 'getAuthenticationRequests' )
3457 ->willReturnCallback( static function ( $action ) use ( $type, $makeReq ) {
3458 return [ $makeReq( "primary-$type-$action" ), $makeReq( 'generic' ) ];
3460 $mocks["primary-$type"]->method( 'providerAllowsAuthenticationDataChange' )
3461 ->willReturn( $good );
3462 $this->primaryauthMocks
[] = $mocks["primary-$type"];
3465 $mocks['primary2'] = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3466 $mocks['primary2']->method( 'getUniqueId' )
3467 ->willReturn( 'primary2' );
3468 $mocks['primary2']->method( 'accountCreationType' )
3469 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
3470 $mocks['primary2']->method( 'getAuthenticationRequests' )
3472 $mocks['primary2']->method( 'providerAllowsAuthenticationDataChange' )
3473 ->willReturnCallback( static function ( $req ) use ( $good ) {
3474 return $req->getUniqueId() === 'generic' ? StatusValue
::newFatal( 'no' ) : $good;
3476 $this->primaryauthMocks
[] = $mocks['primary2'];
3478 $this->preauthMocks
= [ $mocks['pre'] ];
3479 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3480 $this->initializeManager( true );
3483 if ( isset( $state['continueRequests'] ) ) {
3484 $state['continueRequests'] = array_map( $makeReq, $state['continueRequests'] );
3486 if ( $action === AuthManager
::ACTION_LOGIN_CONTINUE
) {
3487 $this->request
->getSession()->setSecret( AuthManager
::AUTHN_STATE
, $state );
3488 } elseif ( $action === AuthManager
::ACTION_CREATE_CONTINUE
) {
3489 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_CREATION_STATE
, $state );
3490 } elseif ( $action === AuthManager
::ACTION_LINK_CONTINUE
) {
3491 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_LINK_STATE
, $state );
3495 $expectReqs = array_map( $makeReq, $expect );
3496 if ( $action === AuthManager
::ACTION_LOGIN
) {
3497 $req = new RememberMeAuthenticationRequest
;
3498 $req->action
= $action;
3499 $req->required
= AuthenticationRequest
::REQUIRED
;
3500 $expectReqs[] = $req;
3501 } elseif ( $action === AuthManager
::ACTION_CREATE
) {
3502 $req = new UsernameAuthenticationRequest
;
3503 $req->action
= $action;
3504 $expectReqs[] = $req;
3505 $req = new UserDataAuthenticationRequest
;
3506 $req->action
= $action;
3507 $req->required
= AuthenticationRequest
::REQUIRED
;
3508 $expectReqs[] = $req;
3510 usort( $expectReqs, $cmpReqs );
3512 $actual = $this->manager
->getAuthenticationRequests( $action );
3513 foreach ( $actual as $req ) {
3514 // Don't test this here.
3515 $req->required
= AuthenticationRequest
::REQUIRED
;
3517 usort( $actual, $cmpReqs );
3519 $this->assertEquals( $expectReqs, $actual );
3521 // Test CreationReasonAuthenticationRequest gets returned
3522 if ( $action === AuthManager
::ACTION_CREATE
) {
3523 $req = new CreationReasonAuthenticationRequest
;
3524 $req->action
= $action;
3525 $req->required
= AuthenticationRequest
::REQUIRED
;
3526 $expectReqs[] = $req;
3527 usort( $expectReqs, $cmpReqs );
3529 $user = $this->getTestSysop()->getUser();
3530 $actual = $this->manager
->getAuthenticationRequests( $action, $user );
3531 foreach ( $actual as $req ) {
3532 // Don't test this here.
3533 $req->required
= AuthenticationRequest
::REQUIRED
;
3535 usort( $actual, $cmpReqs );
3537 $this->assertEquals( $expectReqs, $actual );
3541 public static function provideGetAuthenticationRequests() {
3544 AuthManager
::ACTION_LOGIN
,
3545 [ 'pre-login', 'primary-none-login', 'primary-create-login',
3546 'primary-link-login', 'secondary-login', 'generic' ],
3549 AuthManager
::ACTION_CREATE
,
3550 [ 'pre-create', 'primary-none-create', 'primary-create-create',
3551 'primary-link-create', 'secondary-create', 'generic' ],
3554 AuthManager
::ACTION_LINK
,
3555 [ 'primary-link-link', 'generic' ],
3558 AuthManager
::ACTION_CHANGE
,
3559 [ 'primary-none-change', 'primary-create-change', 'primary-link-change',
3560 'secondary-change' ],
3563 AuthManager
::ACTION_REMOVE
,
3564 [ 'primary-none-remove', 'primary-create-remove', 'primary-link-remove',
3565 'secondary-remove' ],
3568 AuthManager
::ACTION_UNLINK
,
3569 [ 'primary-link-remove' ],
3572 AuthManager
::ACTION_LOGIN_CONTINUE
,
3576 AuthManager
::ACTION_LOGIN_CONTINUE
,
3577 $reqs = [ 'continue-login', 'foo', 'bar' ],
3579 'continueRequests' => $reqs,
3583 AuthManager
::ACTION_CREATE_CONTINUE
,
3587 AuthManager
::ACTION_CREATE_CONTINUE
,
3588 $reqs = [ 'continue-create', 'foo', 'bar' ],
3590 'continueRequests' => $reqs,
3594 AuthManager
::ACTION_LINK_CONTINUE
,
3598 AuthManager
::ACTION_LINK_CONTINUE
,
3599 $reqs = [ 'continue-link', 'foo', 'bar' ],
3601 'continueRequests' => $reqs,
3607 public function testGetAuthenticationRequestsRequired() {
3608 $makeReq = function ( $key, $required ) {
3609 $req = $this->createMock( AuthenticationRequest
::class );
3610 $req->method( 'getUniqueId' )
3611 ->willReturn( $key );
3612 $req->action
= AuthManager
::ACTION_LOGIN
;
3613 $req->required
= $required;
3616 $cmpReqs = static function ( $a, $b ) {
3617 $ret = strcmp( get_class( $a ), get_class( $b ) );
3619 $ret = strcmp( $a->getUniqueId(), $b->getUniqueId() );
3624 $primary1 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3625 $primary1->method( 'getUniqueId' )
3626 ->willReturn( 'primary1' );
3627 $primary1->method( 'accountCreationType' )
3628 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
3629 $primary1->method( 'getAuthenticationRequests' )
3630 ->willReturnCallback( static function ( $action ) use ( $makeReq ) {
3632 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3633 $makeReq( "required", AuthenticationRequest
::REQUIRED
),
3634 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3635 $makeReq( "foo", AuthenticationRequest
::REQUIRED
),
3636 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3637 $makeReq( "baz", AuthenticationRequest
::OPTIONAL
),
3641 $primary2 = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3642 $primary2->method( 'getUniqueId' )
3643 ->willReturn( 'primary2' );
3644 $primary2->method( 'accountCreationType' )
3645 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
3646 $primary2->method( 'getAuthenticationRequests' )
3647 ->willReturnCallback( static function ( $action ) use ( $makeReq ) {
3649 $makeReq( "primary-shared", AuthenticationRequest
::REQUIRED
),
3650 $makeReq( "required2", AuthenticationRequest
::REQUIRED
),
3651 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3655 $secondary = $this->createMock( AbstractSecondaryAuthenticationProvider
::class );
3656 $secondary->method( 'getUniqueId' )
3657 ->willReturn( 'secondary' );
3658 $secondary->method( 'getAuthenticationRequests' )
3659 ->willReturnCallback( static function ( $action ) use ( $makeReq ) {
3661 $makeReq( "foo", AuthenticationRequest
::OPTIONAL
),
3662 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3663 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3667 $rememberReq = new RememberMeAuthenticationRequest
;
3668 $rememberReq->action
= AuthManager
::ACTION_LOGIN
;
3670 $this->primaryauthMocks
= [ $primary1, $primary2 ];
3671 $this->secondaryauthMocks
= [ $secondary ];
3672 $this->initializeManager( true );
3674 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3677 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3678 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3679 $makeReq( "required2", AuthenticationRequest
::PRIMARY_REQUIRED
),
3680 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3681 $makeReq( "optional2", AuthenticationRequest
::OPTIONAL
),
3682 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3683 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3684 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3686 usort( $actual, $cmpReqs );
3687 usort( $expected, $cmpReqs );
3688 $this->assertEquals( $expected, $actual );
3690 $this->primaryauthMocks
= [ $primary1 ];
3691 $this->secondaryauthMocks
= [ $secondary ];
3692 $this->initializeManager( true );
3694 $actual = $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LOGIN
);
3697 $makeReq( "primary-shared", AuthenticationRequest
::PRIMARY_REQUIRED
),
3698 $makeReq( "required", AuthenticationRequest
::PRIMARY_REQUIRED
),
3699 $makeReq( "optional", AuthenticationRequest
::OPTIONAL
),
3700 $makeReq( "foo", AuthenticationRequest
::PRIMARY_REQUIRED
),
3701 $makeReq( "bar", AuthenticationRequest
::REQUIRED
),
3702 $makeReq( "baz", AuthenticationRequest
::REQUIRED
),
3704 usort( $actual, $cmpReqs );
3705 usort( $expected, $cmpReqs );
3706 $this->assertEquals( $expected, $actual );
3709 public function testAllowsPropertyChange() {
3711 foreach ( [ 'primary', 'secondary' ] as $key ) {
3712 $class = ucfirst( $key ) . 'AuthenticationProvider';
3713 $mocks[$key] = $this->createMock( "MediaWiki\\Auth\\Abstract$class" );
3714 $mocks[$key]->method( 'getUniqueId' )
3715 ->willReturn( $key );
3716 $mocks[$key]->method( 'providerAllowsPropertyChange' )
3717 ->willReturnCallback( static function ( $prop ) use ( $key ) {
3718 return $prop !== $key;
3722 $this->primaryauthMocks
= [ $mocks['primary'] ];
3723 $this->secondaryauthMocks
= [ $mocks['secondary'] ];
3724 $this->initializeManager( true );
3726 $this->assertTrue( $this->manager
->allowsPropertyChange( 'foo' ) );
3727 $this->assertFalse( $this->manager
->allowsPropertyChange( 'primary' ) );
3728 $this->assertFalse( $this->manager
->allowsPropertyChange( 'secondary' ) );
3731 public function testAutoCreateOnLogin() {
3732 $username = self
::usernameForCreation();
3734 $req = $this->createMock( AuthenticationRequest
::class );
3736 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3737 $mock->method( 'getUniqueId' )->willReturn( 'primary' );
3738 $mock->method( 'beginPrimaryAuthentication' )
3739 ->willReturn( AuthenticationResponse
::newPass( $username ) );
3740 $mock->method( 'accountCreationType' )
3741 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
3742 $mock->method( 'testUserExists' )->willReturn( true );
3743 $mock->method( 'testUserForCreation' )
3744 ->willReturn( StatusValue
::newGood() );
3746 $mock2 = $this->createMock( AbstractSecondaryAuthenticationProvider
::class );
3747 $mock2->method( 'getUniqueId' )
3748 ->willReturn( 'secondary' );
3749 $mock2->method( 'beginSecondaryAuthentication' )
3750 ->willReturn( AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ) );
3751 $mock2->method( 'continueSecondaryAuthentication' )
3752 ->willReturn( AuthenticationResponse
::newAbstain() );
3753 $mock2->method( 'testUserForCreation' )
3754 ->willReturn( StatusValue
::newGood() );
3756 $this->primaryauthMocks
= [ $mock ];
3757 $this->secondaryauthMocks
= [ $mock2 ];
3758 $this->initializeManager( true );
3759 $this->manager
->setLogger( new NullLogger() );
3760 $session = $this->request
->getSession();
3763 $this->assertSame( 0, User
::newFromName( $username )->getId() );
3765 $callback = $this->callback( static function ( $user ) use ( $username ) {
3766 return $user->getName() === $username;
3769 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->never() );
3770 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->once() )
3771 ->with( $callback, true );
3772 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3773 $this->unhook( 'LocalUserCreated' );
3774 $this->unhook( 'UserLoggedIn' );
3775 $this->assertSame( AuthenticationResponse
::UI
, $ret->status
);
3777 $id = (int)User
::newFromName( $username )->getId();
3778 $this->assertNotSame( 0, User
::newFromName( $username )->getId() );
3779 $this->assertSame( 0, $session->getUser()->getId() );
3781 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->once() )
3782 ->with( $callback );
3783 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3784 $ret = $this->manager
->continueAuthentication( [] );
3785 $this->unhook( 'LocalUserCreated' );
3786 $this->unhook( 'UserLoggedIn' );
3787 $this->assertSame( AuthenticationResponse
::PASS
, $ret->status
);
3788 $this->assertSame( $username, $ret->username
);
3789 $this->assertSame( $id, $session->getUser()->getId() );
3792 public function testAutoCreateFailOnLogin() {
3793 $username = self
::usernameForCreation();
3795 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3796 $mock->method( 'getUniqueId' )->willReturn( 'primary' );
3797 $mock->method( 'beginPrimaryAuthentication' )
3798 ->willReturn( AuthenticationResponse
::newPass( $username ) );
3799 $mock->method( 'accountCreationType' )
3800 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
3801 $mock->method( 'testUserExists' )->willReturn( true );
3802 $mock->method( 'testUserForCreation' )
3803 ->willReturn( StatusValue
::newFatal( 'fail-from-primary' ) );
3805 $this->primaryauthMocks
= [ $mock ];
3806 $this->initializeManager( true );
3807 $this->manager
->setLogger( new NullLogger() );
3808 $session = $this->request
->getSession();
3811 $this->assertSame( 0, $session->getUser()->getId() );
3812 $this->assertSame( 0, User
::newFromName( $username )->getId() );
3814 $this->hook( 'UserLoggedIn', UserLoggedInHook
::class, $this->never() );
3815 $this->hook( 'LocalUserCreated', LocalUserCreatedHook
::class, $this->never() );
3816 $ret = $this->manager
->beginAuthentication( [], 'http://localhost/' );
3817 $this->unhook( 'LocalUserCreated' );
3818 $this->unhook( 'UserLoggedIn' );
3819 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3820 $this->assertSame( 'authmanager-authn-autocreate-failed', $ret->message
->getKey() );
3822 $this->assertSame( 0, User
::newFromName( $username )->getId() );
3823 $this->assertSame( 0, $session->getUser()->getId() );
3826 public function testAuthenticationSessionData() {
3827 $this->initializeManager( true );
3829 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3830 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3831 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3832 $this->assertSame( 'foo!', $this->manager
->getAuthenticationSessionData( 'foo' ) );
3833 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3834 $this->manager
->removeAuthenticationSessionData( 'foo' );
3835 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3836 $this->assertSame( 'bar!', $this->manager
->getAuthenticationSessionData( 'bar' ) );
3837 $this->manager
->removeAuthenticationSessionData( 'bar' );
3838 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3840 $this->manager
->setAuthenticationSessionData( 'foo', 'foo!' );
3841 $this->manager
->setAuthenticationSessionData( 'bar', 'bar!' );
3842 $this->manager
->removeAuthenticationSessionData( null );
3843 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'foo' ) );
3844 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'bar' ) );
3847 public function testCanLinkAccounts() {
3849 PrimaryAuthenticationProvider
::TYPE_CREATE
=> false,
3850 PrimaryAuthenticationProvider
::TYPE_LINK
=> true,
3851 PrimaryAuthenticationProvider
::TYPE_NONE
=> false,
3854 foreach ( $types as $type => $can ) {
3855 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3856 $mock->method( 'getUniqueId' )->willReturn( $type );
3857 $mock->method( 'accountCreationType' )
3858 ->willReturn( $type );
3859 $this->primaryauthMocks
= [ $mock ];
3860 $this->initializeManager( true );
3861 $this->assertSame( $can, $this->manager
->canLinkAccounts(), $type );
3865 public function testBeginAccountLink() {
3866 $user = $this->getTestSysop()->getUser();
3867 $this->initializeManager();
3869 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_LINK_STATE
, 'test' );
3871 $this->manager
->beginAccountLink( $user, [], 'http://localhost/' );
3872 $this->fail( 'Expected exception not thrown' );
3873 } catch ( LogicException
$ex ) {
3874 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3876 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
) );
3878 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3879 $mock->method( 'getUniqueId' )->willReturn( 'X' );
3880 $mock->method( 'accountCreationType' )
3881 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
3882 $this->primaryauthMocks
= [ $mock ];
3883 $this->initializeManager( true );
3885 $ret = $this->manager
->beginAccountLink( new User
, [], 'http://localhost/' );
3886 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3887 $this->assertSame( 'noname', $ret->message
->getKey() );
3889 $ret = $this->manager
->beginAccountLink(
3890 User
::newFromName( 'UTDoesNotExist' ), [], 'http://localhost/'
3892 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3893 $this->assertSame( 'authmanager-userdoesnotexist', $ret->message
->getKey() );
3896 public function testContinueAccountLink() {
3897 $user = $this->getTestSysop()->getUser();
3898 $this->initializeManager();
3901 'userid' => $user->getId(),
3902 'username' => $user->getName(),
3907 $this->manager
->continueAccountLink( [] );
3908 $this->fail( 'Expected exception not thrown' );
3909 } catch ( LogicException
$ex ) {
3910 $this->assertEquals( 'Account linking is not possible', $ex->getMessage() );
3913 $mock = $this->createMock( AbstractPrimaryAuthenticationProvider
::class );
3914 $mock->method( 'getUniqueId' )->willReturn( 'X' );
3915 $mock->method( 'accountCreationType' )
3916 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
3917 $mock->method( 'beginPrimaryAccountLink' )
3918 ->willReturn( AuthenticationResponse
::newFail( $this->message( 'fail' ) ) );
3919 $this->primaryauthMocks
= [ $mock ];
3920 $this->initializeManager( true );
3922 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_LINK_STATE
, null );
3923 $ret = $this->manager
->continueAccountLink( [] );
3924 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3925 $this->assertSame( 'authmanager-link-not-in-progress', $ret->message
->getKey() );
3927 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_LINK_STATE
,
3928 [ 'username' => $user->getName() . '<>' ] +
$session );
3929 $ret = $this->manager
->continueAccountLink( [] );
3930 $this->assertSame( AuthenticationResponse
::FAIL
, $ret->status
);
3931 $this->assertSame( 'noname', $ret->message
->getKey() );
3932 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
) );
3934 $id = $user->getId();
3935 $this->request
->getSession()->setSecret( AuthManager
::ACCOUNT_LINK_STATE
,
3936 [ 'userid' => $id +
1 ] +
$session );
3938 $ret = $this->manager
->continueAccountLink( [] );
3939 $this->fail( 'Expected exception not thrown' );
3940 } catch ( UnexpectedValueException
$ex ) {
3941 $this->assertEquals(
3942 "User \"{$user->getName()}\" is valid, but ID $id !== " . ( $id +
1 ) . '!',
3946 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
) );
3950 * @dataProvider provideAccountLink
3952 public function testAccountLink(
3953 StatusValue
$preTest, array $primaryResponses, array $managerResponses
3955 $user = $this->getTestSysop()->getUser();
3957 $this->initializeManager();
3959 // Set up lots of mocks...
3960 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
3963 foreach ( [ 'pre', 'primary' ] as $key ) {
3964 $class = ucfirst( $key ) . 'AuthenticationProvider';
3965 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\Abstract$class" )
3966 ->setMockClassName( "MockAbstract$class" )
3968 $mocks[$key]->method( 'getUniqueId' )
3969 ->willReturn( $key );
3971 for ( $i = 2; $i <= 3; $i++
) {
3972 $mocks[$key . $i] = $this->getMockBuilder( "MediaWiki\\Auth\\Abstract$class" )
3973 ->setMockClassName( "MockAbstract$class" )
3975 $mocks[$key . $i]->method( 'getUniqueId' )
3976 ->willReturn( $key . $i );
3980 $mocks['pre']->method( 'testForAccountLink' )
3981 ->willReturnCallback(
3983 use ( $user, $preTest )
3985 $this->assertSame( $user->getId(), $u->getId() );
3986 $this->assertSame( $user->getName(), $u->getName() );
3991 $mocks['pre2']->expects( $this->atMost( 1 ) )->method( 'testForAccountLink' )
3992 ->willReturn( StatusValue
::newGood() );
3994 $mocks['primary']->method( 'accountCreationType' )
3995 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
3996 $ct = count( $primaryResponses );
3997 $callback = $this->returnCallback( function ( $u, $reqs ) use ( $user, $req, &$primaryResponses ) {
3998 $this->assertSame( $user->getId(), $u->getId() );
3999 $this->assertSame( $user->getName(), $u->getName() );
4001 foreach ( $reqs as $r ) {
4002 $this->assertSame( $user->getName(), $r->username
);
4003 $foundReq = $foundReq ||
get_class( $r ) === get_class( $req );
4005 $this->assertTrue( $foundReq, '$reqs contains $req' );
4006 return array_shift( $primaryResponses );
4008 $mocks['primary']->expects( $this->exactly( min( 1, $ct ) ) )
4009 ->method( 'beginPrimaryAccountLink' )
4010 ->will( $callback );
4011 $mocks['primary']->expects( $this->exactly( max( 0, $ct - 1 ) ) )
4012 ->method( 'continuePrimaryAccountLink' )
4013 ->will( $callback );
4015 $abstain = AuthenticationResponse
::newAbstain();
4016 $mocks['primary2']->method( 'accountCreationType' )
4017 ->willReturn( PrimaryAuthenticationProvider
::TYPE_LINK
);
4018 $mocks['primary2']->expects( $this->atMost( 1 ) )->method( 'beginPrimaryAccountLink' )
4019 ->willReturn( $abstain );
4020 $mocks['primary2']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
4021 $mocks['primary3']->method( 'accountCreationType' )
4022 ->willReturn( PrimaryAuthenticationProvider
::TYPE_CREATE
);
4023 $mocks['primary3']->expects( $this->never() )->method( 'beginPrimaryAccountLink' );
4024 $mocks['primary3']->expects( $this->never() )->method( 'continuePrimaryAccountLink' );
4026 $this->preauthMocks
= [ $mocks['pre'], $mocks['pre2'] ];
4027 $this->primaryauthMocks
= [ $mocks['primary3'], $mocks['primary2'], $mocks['primary'] ];
4028 $this->logger
= new TestLogger( true, static function ( $message, $level ) {
4029 return $level === LogLevel
::DEBUG ?
null : $message;
4031 $this->initializeManager( true );
4033 $constraint = Assert
::logicalOr(
4034 $this->equalTo( AuthenticationResponse
::PASS
),
4035 $this->equalTo( AuthenticationResponse
::FAIL
)
4037 $providers = array_merge( $this->preauthMocks
, $this->primaryauthMocks
);
4038 foreach ( $providers as $p ) {
4039 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', false );
4040 $p->expects( $this->atMost( 1 ) )->method( 'postAccountLink' )
4041 ->willReturnCallback( function ( $userArg, $response ) use ( $user, $constraint, $p ) {
4042 $this->assertInstanceOf( User
::class, $userArg );
4043 $this->assertSame( $user->getName(), $userArg->getName() );
4044 $this->assertInstanceOf( AuthenticationResponse
::class, $response );
4045 $this->assertThat( $response->status
, $constraint );
4046 DynamicPropertyTestHelper
::setDynamicProperty( $p, 'postCalled', $response->status
);
4052 foreach ( $managerResponses as $i => $response ) {
4053 if ( $response instanceof AuthenticationResponse
&&
4054 $response->status
=== AuthenticationResponse
::PASS
4056 $expectLog[] = [ LogLevel
::INFO
, 'Account linked to {user} by primary' ];
4061 $ret = $this->manager
->beginAccountLink( $user, [ $req ], 'http://localhost/' );
4063 $ret = $this->manager
->continueAccountLink( [ $req ] );
4065 if ( $response instanceof Exception
) {
4066 $this->fail( 'Expected exception not thrown', "Response $i" );
4068 } catch ( Exception
$ex ) {
4069 if ( !$response instanceof Exception
) {
4072 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
4073 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
),
4074 "Response $i, exception, session state" );
4078 $this->assertSame( 'http://localhost/', $req->returnToUrl
);
4080 $ret->message
= $this->message( $ret->message
);
4081 $this->assertResponseEquals( $response, $ret, "Response $i, response" );
4082 if ( $response->status
=== AuthenticationResponse
::PASS ||
4083 $response->status
=== AuthenticationResponse
::FAIL
4085 $this->assertNull( $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
),
4086 "Response $i, session state" );
4087 foreach ( $providers as $p ) {
4088 $this->assertSame( $response->status
, DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ),
4089 "Response $i, post-auth callback called" );
4092 $this->assertNotNull(
4093 $this->request
->getSession()->getSecret( AuthManager
::ACCOUNT_LINK_STATE
),
4094 "Response $i, session state"
4096 foreach ( $ret->neededRequests
as $neededReq ) {
4097 $this->assertEquals( AuthManager
::ACTION_LINK
, $neededReq->action
,
4098 "Response $i, neededRequest action" );
4100 $this->assertEquals(
4101 $ret->neededRequests
,
4102 $this->manager
->getAuthenticationRequests( AuthManager
::ACTION_LINK_CONTINUE
),
4103 "Response $i, continuation check"
4105 foreach ( $providers as $p ) {
4106 $this->assertFalse( DynamicPropertyTestHelper
::getDynamicProperty( $p, 'postCalled' ), "Response $i, post-auth callback not called" );
4113 $this->assertSame( $expectLog, $this->logger
->getBuffer() );
4116 public function provideAccountLink() {
4117 $req = $this->getMockForAbstractClass( AuthenticationRequest
::class );
4118 $good = StatusValue
::newGood();
4121 'Pre-link test fail in pre' => [
4122 StatusValue
::newFatal( 'fail-from-pre' ),
4125 AuthenticationResponse
::newFail( $this->message( 'fail-from-pre' ) ),
4128 'Failure in primary' => [
4131 AuthenticationResponse
::newFail( $this->message( 'fail-from-primary' ) ),
4135 'All primary abstain' => [
4138 AuthenticationResponse
::newAbstain(),
4141 AuthenticationResponse
::newFail( $this->message( 'authmanager-link-no-primary' ) )
4144 'Primary UI, then redirect, then fail' => [
4147 AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
4148 AuthenticationResponse
::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
4149 AuthenticationResponse
::newFail( $this->message( 'fail-in-primary-continue' ) ),
4153 'Primary redirect, then abstain' => [
4156 $tmp = AuthenticationResponse
::newRedirect(
4157 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
4159 AuthenticationResponse
::newAbstain(),
4163 new DomainException(
4164 'MockAbstractPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
4168 'Primary UI, then pass' => [
4171 $tmp1 = AuthenticationResponse
::newUI( [ $req ], $this->message( '...' ) ),
4172 AuthenticationResponse
::newPass(),
4176 AuthenticationResponse
::newPass( '' ),
4182 AuthenticationResponse
::newPass( '' ),
4185 AuthenticationResponse
::newPass( '' ),
4191 public function testSetRequestContextUserFromSessionUser() {
4192 $user = $this->getTestUser()->getUser();
4193 $context = RequestContext
::getMain();
4194 $context->setUser( $this->getTestUser()->getUser() );
4195 $context->getRequest()->getSession()->setUser( $user );
4196 $this->assertSame( $context->getRequest()->getSession()->getUser()->getName(), $context->getUser()->getName() );
4198 // Update the session with a new user, but leave the context user as the old user
4199 $newSessionUser = $this->getTestUser( 'sysop' )->getUser();
4200 $context->getRequest()->getSession()->setUser( $newSessionUser );
4201 $this->assertNotSame( $newSessionUser->getName(), $context->getUser()->getName() );
4203 $authManager = $this->getServiceContainer()->getAuthManager();
4204 $authManager->setRequestContextUserFromSessionUser();
4205 $this->assertSame( $context->getRequest()->getSession()->getUser()->getName(), $newSessionUser->getName() );
4206 $this->assertSame( $context->getRequest()->getSession()->getUser()->getName(), $context->getUser()->getName() );
4210 * @dataProvider provideAccountCreationAuthenticationRequestTestCases
4212 * @param Closure $userProvider Closure returning the user performing the account creation
4213 * @param string[] $expectedReqsByLevel Map of expected auth request classes keyed by requirement level
4215 public function testDefaultAccountCreationAuthenticationRequests(
4216 Closure
$userProvider,
4217 array $expectedReqsByLevel
4219 // Test the default primary and secondary authentication providers
4220 // irrespective of any potentially conflicting local configuration.
4221 $authConfig = $this->getServiceContainer()
4223 ->getDefaultFor( MainConfigNames
::AuthManagerAutoConfig
);
4225 $this->overrideConfigValues( [
4226 MainConfigNames
::AuthManagerConfig
=> null,
4227 MainConfigNames
::AuthManagerAutoConfig
=> $authConfig
4230 $authManager = $this->getServiceContainer()->getAuthManager();
4231 $userProvider = $userProvider->bindTo( $this );
4233 $reqs = $authManager->getAuthenticationRequests( AuthManager
::ACTION_CREATE
, $userProvider() );
4236 foreach ( $reqs as $req ) {
4237 $reqsByLevel[$req->required
][] = get_class( $req );
4240 foreach ( $expectedReqsByLevel as $level => $expectedReqs ) {
4241 $reqs = $reqsByLevel[$level] ??
[];
4243 sort( $expectedReqs );
4245 $this->assertSame( $expectedReqs, $reqs );
4249 public static function provideAccountCreationAuthenticationRequestTestCases(): iterable
{
4250 // phpcs:disable Squiz.Scope.StaticThisUsage.Found
4251 yield
'account creation on behalf of anonymous user' => [
4252 fn (): User
=> $this->getServiceContainer()->getUserFactory()->newAnonymous( '127.0.0.1' ),
4254 AuthenticationRequest
::OPTIONAL
=> [],
4255 AuthenticationRequest
::REQUIRED
=> [
4256 UserDataAuthenticationRequest
::class,
4257 UsernameAuthenticationRequest
::class
4259 AuthenticationRequest
::PRIMARY_REQUIRED
=> [ PasswordAuthenticationRequest
::class ]
4263 yield
'account creation on behalf of temporary user' => [
4265 $req = new FauxRequest();
4266 return $this->getServiceContainer()
4267 ->getTempUserCreator()
4268 ->create( null, $req )
4272 AuthenticationRequest
::OPTIONAL
=> [],
4273 AuthenticationRequest
::REQUIRED
=> [
4274 UserDataAuthenticationRequest
::class,
4275 UsernameAuthenticationRequest
::class
4277 AuthenticationRequest
::PRIMARY_REQUIRED
=> [ PasswordAuthenticationRequest
::class ]
4281 yield
'account creation on behalf of registered user' => [
4282 fn (): User
=> $this->getTestUser()->getUser(),
4284 AuthenticationRequest
::OPTIONAL
=> [ CreationReasonAuthenticationRequest
::class ],
4285 AuthenticationRequest
::REQUIRED
=> [
4286 UserDataAuthenticationRequest
::class,
4287 UsernameAuthenticationRequest
::class
4289 AuthenticationRequest
::PRIMARY_REQUIRED
=> [
4290 PasswordAuthenticationRequest
::class,
4291 TemporaryPasswordAuthenticationRequest
::class