Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / auth / AuthManagerTest.php
blobf48f512292bbe3f0ee5c01ca7a2c1a23a4a6530b
1 <?php
3 namespace MediaWiki\Tests\Auth;
5 use Closure;
6 use DatabaseLogEntry;
7 use DomainException;
8 use DummySessionProvider;
9 use DynamicPropertyTestHelper;
10 use Exception;
11 use InvalidArgumentException;
12 use LogicException;
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;
73 use Psr\Log\LogLevel;
74 use Psr\Log\NullLogger;
75 use ReflectionClass;
76 use RuntimeException;
77 use StatusValue;
78 use TestLogger;
79 use TestUser;
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;
89 /**
90 * @group AuthManager
91 * @group Database
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" ] )
136 ->getMock();
137 $this->hookContainer->register( $hook, $mock );
138 return $mock->expects( $expect )->method( "on$hook" );
142 * Unsets a 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
155 * @return Message
157 protected function message( $key, $params = [] ) {
158 if ( $key === null ) {
159 return 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
172 * caches are reset.
174 * @param AuthenticationResponse $expected
175 * @param AuthenticationResponse $actual
176 * @param string $msg
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(),
186 $usedMsg );
187 } else {
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() {
199 $config = [
200 'preauth' => [
202 'primaryauth' => [
204 'secondaryauth' => [
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 ) {
212 return $mock;
213 } ];
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(
240 new ServiceOptions(
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 ) {
252 return '127.0.0.1';
255 $this->watchlistManager ??= $this->getServiceContainer()->getWatchlistManager();
256 $this->hookContainer ??= new HookContainer(
257 new StaticHookRegistry( [], [], [] ),
258 $this->objectFactory
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(
276 $this->request,
277 $this->config,
278 $this->objectFactory,
279 $this->hookContainer,
280 $this->readOnlyMode,
281 $this->userNameUtils,
282 $this->blockManager,
283 $this->watchlistManager,
284 $this->loadBalancer,
285 $this->contentLanguage,
286 $this->languageConverterFactory,
287 $this->botPasswordStore,
288 $this->userFactory,
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 )
316 ->getMock();
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 ) {
327 return $provider;
328 } ],
329 ] );
331 $manager = new SessionManager( [
332 'config' => $this->config,
333 'logger' => new NullLogger(),
334 'store' => new HashBagOStuff(),
335 ] );
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() {
360 $mocks = [
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();
396 $provideUser = null;
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,
407 'persisted' => true,
408 'userInfo' => UserInfo::newFromUser( $provideUser, true )
409 ] );
410 } );
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 );
431 try {
432 $this->manager->securitySensitiveOperationStatus( 'foo' );
433 $this->fail( 'Expected exception not thrown' );
434 } catch ( UnexpectedValueException $ex ) {
435 $this->assertSame(
436 $mutableSession
437 ? '$wgReauthenticateTime lacks a default'
438 : '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default',
439 $ex->getMessage()
443 if ( $mutableSession ) {
444 $this->config->set( MainConfigNames::ReauthenticateTime, [
445 'test' => 100,
446 'test2' => -1,
447 'default' => 10,
448 ] );
450 // Mismatched user ID
451 $session->set( 'AuthManager:lastAuthId', $user->getId() + 1 );
452 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
453 $this->assertSame(
454 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
456 $this->assertSame(
457 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
459 $this->assertSame(
460 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
463 // Missing time
464 $session->set( 'AuthManager:lastAuthId', $user->getId() );
465 $session->set( 'AuthManager:lastAuthTimestamp', null );
466 $this->assertSame(
467 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
469 $this->assertSame(
470 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'test' )
472 $this->assertSame(
473 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test2' )
476 // Recent enough to pass
477 $session->set( 'AuthManager:lastAuthTimestamp', time() - 5 );
478 $this->assertSame(
479 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
482 // Not recent enough to pass
483 $session->set( 'AuthManager:lastAuthTimestamp', time() - 20 );
484 $this->assertSame(
485 AuthManager::SEC_REAUTH, $this->manager->securitySensitiveOperationStatus( 'foo' )
487 // But recent enough for the 'test' operation
488 $this->assertSame(
489 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'test' )
491 } else {
492 $this->config->set( MainConfigNames::AllowSecuritySensitiveOperationIfCannotReauthenticate, [
493 'test' => false,
494 'default' => true,
495 ] );
497 $this->assertEquals(
498 AuthManager::SEC_OK, $this->manager->securitySensitiveOperationStatus( 'foo' )
501 $this->assertEquals(
502 AuthManager::SEC_FAIL, $this->manager->securitySensitiveOperationStatus( 'test' )
506 // Test hook, all three possible values
507 foreach ( [
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,
514 $this->exactly( 2 )
516 ->with(
517 /* $status */ $this->anything(),
518 /* $operation */ $this->anything(),
519 /* $session */ $this->callback( static function ( $s ) use ( $session ) {
520 return $s->getId() === $session->getId();
521 } ),
522 /* $timeSinceAuth*/ $mutableSession
523 ? $this->equalToWithDelta( 500, 2 )
524 : -1
526 ->willReturnCallback( static function ( &$v ) use ( $hook ) {
527 $v = $hook;
528 return true;
529 } );
530 $session->set( 'AuthManager:lastAuthTimestamp', time() - 500 );
531 $this->assertEquals(
532 $expect, $this->manager->securitySensitiveOperationStatus( 'test' ), "hook $hook"
534 $this->assertEquals(
535 $expect, $this->manager->securitySensitiveOperationStatus( 'test2' ), "hook $hook"
537 $this->unhook( 'SecuritySensitiveOperationStatus' );
540 ScopedCallback::consume( $reset );
543 public static function provideSecuritySensitiveOperationStatus() {
544 return [
545 [ true ],
546 [ false ],
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' )
562 ->with( $userName )
563 ->willReturn( $primary1Can );
564 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider::class );
565 $mock2->method( 'getUniqueId' )
566 ->willReturn( 'primary2' );
567 $mock2->method( 'testUserCanAuthenticate' )
568 ->with( $userName )
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() {
577 return [
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' )
593 ->with( $userName );
594 $this->primaryauthMocks = [ $mock ];
596 $this->initializeManager( true );
597 $this->logger->setCollect( true );
599 $this->manager->revokeAccessForUser( $userName );
601 $this->assertSame( [
602 [ LogLevel::INFO, 'Revoking access for {user}' ],
603 ], $this->logger->getBuffer() );
606 public function testProviderCreation() {
607 $mocks = [
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'] ];
620 // Normal operation
621 $this->initializeManager();
622 $this->assertSame(
623 $mocks['primary'],
624 $this->managerPriv->getAuthenticationProvider( 'primary' )
626 $this->assertSame(
627 $mocks['secondary'],
628 $this->managerPriv->getAuthenticationProvider( 'secondary' )
630 $this->assertSame(
631 $mocks['pre'],
632 $this->managerPriv->getAuthenticationProvider( 'pre' )
634 $this->assertSame(
635 [ 'pre' => $mocks['pre'] ],
636 $this->managerPriv->getPreAuthenticationProviders()
638 $this->assertSame(
639 [ 'primary' => $mocks['primary'] ],
640 $this->managerPriv->getPrimaryAuthenticationProviders()
642 $this->assertSame(
643 [ 'secondary' => $mocks['secondary'] ],
644 $this->managerPriv->getSecondaryAuthenticationProviders()
647 // Duplicate IDs
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 );
656 try {
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 );
662 $this->assertSame(
663 "Duplicate specifications for id X (classes $class2 and $class1)", $ex->getMessage()
667 // Wrong classes
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 );
675 try {
676 $this->managerPriv->getPreAuthenticationProviders();
677 $this->fail( 'Expected exception not thrown' );
678 } catch ( RuntimeException $ex ) {
679 $this->assertSame(
680 "Expected instance of MediaWiki\\Auth\\PreAuthenticationProvider, got $class",
681 $ex->getMessage()
684 $this->preauthMocks = [];
685 $this->primaryauthMocks = [ $mock ];
686 $this->secondaryauthMocks = [];
687 $this->initializeManager( true );
688 try {
689 $this->managerPriv->getPrimaryAuthenticationProviders();
690 $this->fail( 'Expected exception not thrown' );
691 } catch ( RuntimeException $ex ) {
692 $this->assertSame(
693 "Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got $class",
694 $ex->getMessage()
697 $this->preauthMocks = [];
698 $this->primaryauthMocks = [];
699 $this->secondaryauthMocks = [ $mock ];
700 $this->initializeManager( true );
701 try {
702 $this->managerPriv->getSecondaryAuthenticationProviders();
703 $this->fail( 'Expected exception not thrown' );
704 } catch ( RuntimeException $ex ) {
705 $this->assertSame(
706 "Expected instance of MediaWiki\\Auth\\SecondaryAuthenticationProvider, got $class",
707 $ex->getMessage()
711 // Sorting
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 );
725 $this->assertSame(
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 );
734 $this->assertSame(
735 [ 'C' => $mock3, 'B' => $mock2, 'A' => $mock1 ],
736 $this->managerPriv->getPrimaryAuthenticationProviders()
739 // filtering
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 ];
761 } );
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() );
785 $this->assertSame(
786 $expectedLang,
787 $this->userOptionsManager->getOption( $user, 'language' )
789 $this->assertSame(
790 $expectedVariant,
791 $this->userOptionsManager->getOption( $user, 'variant' )
795 public static function provideSetDefaultUserOptions() {
796 return [
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' );
818 $this->assertSame(
819 [ 'B' => $mockB ], $this->managerPriv->getPrimaryAuthenticationProviders()
821 $this->assertSame( null, $this->managerPriv->getAuthenticationProvider( 'A' ) );
822 $this->assertSame( $mockB, $this->managerPriv->getAuthenticationProvider( 'B' ) );
823 $this->assertSame( [
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' );
836 $this->assertSame(
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 ) );
842 $this->assertNull(
843 $this->request->getSession()->getSecret( AuthManager::ACCOUNT_CREATION_STATE )
845 $this->assertSame( [
846 [ LogLevel::WARNING, 'Overriding AuthManager primary authn because testing' ],
848 LogLevel::WARNING,
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();
856 try {
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 );
863 $this->assertSame(
864 "Duplicate specifications for id B (classes $class2 and $class1)", $ex->getMessage()
868 // Wrong classes
869 $mock = $this->getMockForAbstractClass( AuthenticationProvider::class );
870 $mock->method( 'getUniqueId' )->willReturn( 'X' );
871 $class = get_class( $mock );
872 try {
873 $this->manager->forcePrimaryAuthenticationProviders( [ $mock ], 'testing' );
874 $this->fail( 'Expected exception not thrown' );
875 } catch ( RuntimeException $ex ) {
876 $this->assertSame(
877 "Expected instance of MediaWiki\\Auth\\AbstractPrimaryAuthenticationProvider, got $class",
878 $ex->getMessage()
883 public function testBeginAuthentication() {
884 $this->initializeManager();
886 // Immutable session
887 [ $provider, $reset ] = $this->getMockSessionProvider( false );
888 $this->hook( 'UserLoggedIn', UserLoggedInHook::class, $this->never() );
889 $this->request->getSession()->setSecret( AuthManager::AUTHN_STATE, 'test' );
890 try {
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();
903 $reqs = [
904 new CreatedAccountAuthenticationRequest( $user->getId(), $user->getName() )
906 $this->hook( 'UserLoggedIn', UserLoggedInHook::class, $this->never() );
907 try {
908 $this->manager->beginAuthentication( $reqs, 'http://localhost/' );
909 $this->fail( 'Expected exception not thrown' );
910 } catch ( LogicException $ex ) {
911 $this->assertSame(
912 'CreatedAccountAuthenticationRequests are only valid on the same AuthManager ' .
913 'that created the account',
914 $ex->getMessage()
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();
925 } ) );
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(
938 time(),
939 $this->request->getSessionData( 'AuthManager:lastAuthTimestamp' ),
941 'timestamp ±1'
943 $this->assertNull( $this->request->getSession()->getSecret( AuthManager::AUTHN_STATE ) );
944 $this->assertSame( $user->getId(), $this->request->getSession()->getUser()->getId() );
945 $this->assertSame( [
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 );
1060 $mocks = [];
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" )
1065 ->getMock();
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' )
1077 ->willReturn( [] );
1080 $mocks['pre']->expects( $this->once() )->method( 'testForAuthentication' )
1081 ->willReturnCallback( function ( $reqs ) use ( $req, $preResponse ) {
1082 $this->assertContains( $req, $reqs );
1083 return $preResponse;
1084 } );
1086 $ct = count( $primaryResponses );
1087 $callback = $this->returnCallback( function ( $reqs ) use ( $req, &$primaryResponses ) {
1088 $this->assertContains( $req, $reqs );
1089 return array_shift( $primaryResponses );
1090 } );
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 );
1097 if ( $link ) {
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 );
1108 } );
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(
1144 array_merge(
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 );
1162 } );
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;
1171 if ( $success ) {
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;
1175 } ) );
1176 } else {
1177 $this->hook( 'UserLoggedIn', UserLoggedInHook::class, $this->never() );
1179 if ( $success || (
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() );
1188 } else {
1189 $this->hook( 'AuthManagerLoginAuthenticateAudit',
1190 AuthManagerLoginAuthenticateAuditHook::class, $this->never() );
1193 try {
1194 if ( !$i ) {
1195 $ret = $this->manager->beginAuthentication( [ $req ], 'http://localhost/' );
1196 } else {
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 ) {
1204 throw $ex;
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' );
1211 return;
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" );
1221 if ( $success ) {
1222 $this->assertSame( $id, $session->getUser()->getId(),
1223 "Response $i, authn" );
1224 } else {
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" );
1235 } else {
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,
1257 $maybeLink,
1258 "Response $i, maybeLink"
1260 } else {
1261 $this->assertEquals( [], $maybeLink, "Response $i, maybeLink" );
1265 if ( $success ) {
1266 $this->assertSame( $req->rememberMe, $session->shouldRememberUser(),
1267 'rememberMe checkbox had effect' );
1268 } else {
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';
1298 return [
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(),
1312 $tmp = [
1313 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
1316 $tmp
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(),
1330 $tmp = [
1331 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1332 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
1333 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
1336 $tmp
1338 'Primary redirect, then abstain' => [
1339 StatusValue::newGood(),
1341 $tmp = AuthenticationResponse::newRedirect(
1342 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
1344 AuthenticationResponse::newAbstain(),
1348 $tmp,
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 ),
1362 $tmp,
1363 $restartResponse,
1366 'Primary UI, then pass with no local user (link type)' => [
1367 StatusValue::newGood(),
1369 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1370 $restartResponse2Pass,
1374 $tmp,
1375 $restartResponse2,
1377 true
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 ),
1396 $tmp = [
1397 AuthenticationResponse::newFail( $this->message( 'fail-in-secondary' ) ),
1399 $tmp
1401 'Secondary UI, then abstain' => [
1402 StatusValue::newGood(),
1404 AuthenticationResponse::newPass( $userNamePlaceholder ),
1407 $tmp = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
1408 AuthenticationResponse::newAbstain()
1411 $tmp,
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();
1434 $primaryConfig = [
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
1467 $updateManager();
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'] );
1475 } );
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
1483 $updateManager();
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' ) );
1487 return false;
1488 } );
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
1495 $updateManager();
1496 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook::class, $this->once() );
1497 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1498 $response = 'invalid';
1499 return false;
1500 } );
1501 try {
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' );
1509 $updateManager();
1510 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook::class, $this->once() );
1511 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1512 $response = AuthenticationResponse::newPass( 'UTDummy' );
1513 } );
1514 try {
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' );
1522 $updateManager();
1523 $hook = $this->hook( 'AuthManagerVerifyAuthentication', AuthManagerVerifyAuthenticationHook::class, $this->once() );
1524 $hook->willReturnCallback( static function ( $user, &$response, $authManager, $info ) {
1525 return false;
1526 } );
1527 try {
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'] );
1538 $updateManager();
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' ) );
1543 return false;
1544 } );
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' )
1564 ->with( $userName )
1565 ->willReturn( $primary1Exists );
1566 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider::class );
1567 $mock2->method( 'getUniqueId' )
1568 ->willReturn( 'primary2' );
1569 $mock2->method( 'testUserExists' )
1570 ->with( $userName )
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() {
1579 return [
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' )
1599 ->with( $req )
1600 ->willReturn( $primaryReturn );
1601 $mock2 = $this->createMock( AbstractSecondaryAuthenticationProvider::class );
1602 $mock2->method( 'getUniqueId' )->willReturn( '2' );
1603 $mock2->method( 'providerAllowsAuthenticationDataChange' )
1604 ->with( $req )
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' );
1624 return [
1626 StatusValue::newGood(),
1627 StatusValue::newGood(),
1628 Status::newGood(),
1631 StatusValue::newGood(),
1632 StatusValue::newGood( 'ignore' ),
1633 Status::newGood(),
1636 StatusValue::newGood( 'ignored' ),
1637 StatusValue::newGood(),
1638 Status::newGood(),
1641 StatusValue::newGood( 'ignored' ),
1642 StatusValue::newGood( 'ignored' ),
1643 $ignored,
1646 StatusValue::newFatal( 'fail from primary' ),
1647 StatusValue::newGood(),
1648 Status::newFatal( 'fail from primary' ),
1651 $okFromPrimary,
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(),
1662 $okFromSecondary,
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' )
1680 ->with( $req );
1681 $mock2 = $this->createMock( AbstractPrimaryAuthenticationProvider::class );
1682 $mock2->method( 'getUniqueId' )->willReturn( '2' );
1683 $mock2->expects( $this->once() )->method( 'providerChangeAuthenticationData' )
1684 ->with( $req );
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() {
1696 $types = [
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();
1775 $blockOptions = [
1776 'address' => $user,
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();
1798 $blockOptions = [
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 => [
1819 'localhost',
1821 MainConfigNames::ProxyWhitelist => [],
1822 ] );
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();
1852 $blockOptions = [
1853 'address' => $user,
1854 'by' => $this->getTestSysop()->getUser(),
1855 'reason' => __METHOD__,
1856 'expiry' => time() + 100500,
1857 'createAccount' => false,
1859 $block = new DatabaseBlock( $blockOptions );
1860 $blockStore->insertBlock( $block );
1862 $blockOptions = [
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
1881 * @return string
1883 private static function usernameForCreation( $uniq = '' ) {
1884 $i = 0;
1885 do {
1886 $username = "UTAuthManagerTestAccountCreation" . $uniq . ++$i;
1887 } while ( User::newFromName( $username )->getId() !== 0 );
1888 return $username;
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(
1937 Status::newGood(),
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;
1962 } );
1963 $this->initializeManager();
1965 $this->request->getSession()->setSecret( AuthManager::ACCOUNT_CREATION_STATE, 'test' );
1966 $this->hook( 'LocalUserCreated', LocalUserCreatedHook::class, $this->never() );
1967 try {
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' );
1976 $this->assertNull(
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' ] )
2080 ->getMock();
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;
2111 } );
2112 $this->initializeManager();
2114 $session = [
2115 'userid' => 0,
2116 'username' => $username,
2117 'creatorid' => 0,
2118 'creatorname' => $username,
2119 'reqs' => [],
2120 'providerIds' => [ 'preauth' => [], 'primaryauth' => [], 'secondaryauth' => [] ],
2121 'primary' => null,
2122 'primaryResponse' => null,
2123 'secondary' => [],
2124 'ranPreTests' => true,
2127 $this->hook( 'LocalUserCreated', LocalUserCreatedHook::class, $this->never() );
2128 try {
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() );
2161 $this->assertNull(
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( [] );
2171 unset( $lock );
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.
2177 $this->assertSame(
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() );
2200 $this->assertNull(
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() );
2207 try {
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' );
2214 $this->assertNull(
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() );
2223 try {
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' );
2232 $this->assertNull(
2233 $this->request->getSession()->getSecret( AuthManager::ACCOUNT_CREATION_STATE )
2236 $req = $this->getMockBuilder( UserDataAuthenticationRequest::class )
2237 ->onlyMethods( [ 'populateUser' ] )
2238 ->getMock();
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() );
2246 $this->assertNull(
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 );
2271 $mocks = [];
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" )
2276 ->getMock();
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() );
2289 $foundReq = false;
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' ) {
2296 return $preTest;
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() );
2324 $foundReq = false;
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 );
2331 } );
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() );
2343 $foundReq = false;
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 );
2350 } );
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;
2389 } );
2390 $expectLog = [];
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 );
2412 } );
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)' )
2419 ->from( 'logging' )
2420 ->where( [ 'log_type' => 'newusers' ] )
2421 ->fetchField();
2423 $first = true;
2424 $created = false;
2425 foreach ( $managerResponses as $i => $response ) {
2426 $success = $response instanceof AuthenticationResponse &&
2427 $response->status === AuthenticationResponse::PASS;
2428 if ( $i === 'created' ) {
2429 $created = true;
2430 $this->hook( 'LocalUserCreated', LocalUserCreatedHook::class, $this->once() )
2431 ->with(
2432 $this->callback( static function ( $user ) use ( $username ) {
2433 return $user->getName() === $username;
2434 } ),
2435 false
2437 $expectLog[] = [ LogLevel::INFO, "Creating user {user} during account creation" ];
2438 } else {
2439 $this->hook( 'LocalUserCreated', LocalUserCreatedHook::class, $this->never() );
2442 try {
2443 if ( $first ) {
2444 $userReq = new UsernameAuthenticationRequest;
2445 $userReq->username = $username;
2446 $ret = $this->manager->beginAccountCreation(
2447 $creator, [ $userReq, $req ], 'http://localhost/'
2449 } else {
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 ) {
2457 throw $ex;
2459 $this->assertEquals( $response->getMessage(), $ex->getMessage(), "Response $i, exception" );
2460 $this->assertNull(
2461 $this->request->getSession()->getSecret( AuthManager::ACCOUNT_CREATION_STATE ),
2462 "Response $i, exception, session state"
2464 $this->unhook( 'LocalUserCreated' );
2465 return;
2468 $this->unhook( 'LocalUserCreated' );
2470 $this->assertSame( 'http://localhost/', $req->returnToUrl );
2472 if ( $success ) {
2473 $this->assertNotNull( $ret->loginRequest, "Response $i, login marker" );
2474 $this->assertContains(
2475 $ret->loginRequest, $this->managerPriv->createdAccountAuthenticationRequests,
2476 "Response $i, login marker"
2479 $expectLog[] = [
2480 LogLevel::INFO,
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;
2488 } else {
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 ) {
2496 $this->assertNull(
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" );
2504 } else {
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() );
2526 $first = false;
2529 $this->assertSame( $expectLog, $this->logger->getBuffer() );
2531 $this->assertSame(
2532 $maxLogId,
2533 $dbw->newSelectQueryBuilder()
2534 ->select( 'MAX(log_id)' )
2535 ->from( 'logging' )
2536 ->where( [ 'log_type' => 'newusers' ] )
2537 ->fetchField() );
2540 public function provideAccountCreation() {
2541 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
2542 $good = StatusValue::newGood();
2544 return [
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,
2571 $tmp = [
2572 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
2575 $tmp
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,
2589 $tmp = [
2590 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
2591 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
2592 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
2595 $tmp
2597 'Primary redirect, then abstain' => [
2598 $good, $good, $good,
2600 $tmp = AuthenticationResponse::newRedirect(
2601 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
2603 AuthenticationResponse::newAbstain(),
2607 $tmp,
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(),
2623 $tmp1,
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( '' ),
2637 'created' => $tmp1,
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)' )
2697 ->from( 'logging' )
2698 ->where( [ 'log_type' => 'newusers' ] )
2699 ->fetchField();
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() );
2722 $this->assertSame(
2723 $isAnon ? $user->getId() : $creator->getId(),
2724 $entry->getPerformerIdentity()->getId()
2726 $this->assertSame(
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() {
2736 return [
2737 [ true, null ],
2738 [ true, 'foobar' ],
2739 [ false, null ],
2740 [ false, 'byemail' ],
2744 public function testAccountCreation_AuthManagerVerifyAuthentication() {
2745 $this->logger = new NullLogger();
2746 $this->initializeManager();
2748 $primaryConfig = [
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
2784 $updateManager();
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'] );
2792 } );
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'] );
2803 $updateManager();
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' ) );
2809 return false;
2810 } );
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
2823 // the testing.
2824 $workaroundPHPUnitBug = false;
2826 $username = self::usernameForCreation();
2827 $expectedSource = AuthManager::AUTOCREATE_SOURCE_SESSION;
2829 $this->setGroupPermissions( [
2830 '*' => [
2831 'createaccount' => true,
2832 'autocreateaccount' => false,
2834 ] );
2835 $this->initializeManager( true );
2837 // Set up lots of mocks...
2838 $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;
2850 } );
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
2881 $good, $good, $good
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
2894 $good, $good, $good
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 );
2907 return $m;
2908 } );
2909 $this->logger = $logger;
2910 $this->manager->setLogger( $logger );
2912 try {
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
2921 $session->clear();
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();
2936 $session->clear();
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
2952 $session->clear();
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
2970 $session->clear();
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();
2985 $session->clear();
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();
3000 // Invalid name
3001 $session->clear();
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( [
3018 '*' => [
3019 'createaccount' => false,
3020 'autocreateaccount' => false,
3022 ] );
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( [
3064 '*' => [
3065 'createaccount' => true,
3066 'autocreateaccount' => false,
3068 ] );
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();
3078 // Test lock fail
3079 $session->clear();
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 );
3089 unset( $lock );
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
3101 $session->clear();
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 )
3118 $session->clear();
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 )
3135 $session->clear();
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 )
3152 // Test backoff
3153 $cache = $this->objectCacheFactory->getLocalClusterInstance();
3154 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
3155 $cache->set( $backoffKey, true );
3156 $session->clear();
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
3173 $session->clear();
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 ) );
3195 $session->clear();
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 );
3201 try {
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.
3219 $session->clear();
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' );
3229 } );
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 ) );
3245 // Success!
3246 $session->clear();
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)' )
3265 ->from( 'logging' )
3266 ->where( [ 'log_type' => 'newusers' ] )
3267 ->fetchField();
3268 $session->clear();
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();
3283 $this->assertSame(
3284 $maxLogId,
3285 $dbw->newSelectQueryBuilder()
3286 ->select( 'MAX(log_id)' )
3287 ->from( 'logging' )
3288 ->where( [ 'log_type' => 'newusers' ] )
3289 ->fetchField() );
3291 $this->config->set( MainConfigNames::NewUserLog, true );
3292 $session->clear();
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(
3318 string $blockType,
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;
3336 } else {
3337 $this->fail( "Unknown block type \"$blockType\"" );
3340 if ( $blockOptions !== null ) {
3341 $blockOptions += [
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' ) {
3354 $performer = null;
3355 } else {
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() {
3369 return [
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' => [
3374 'ip',
3375 [ 'createAccount' => true ],
3376 'anon',
3377 false
3379 'partially ip-blocked' => [
3380 'ip',
3381 [ 'restrictions' => [ new PageRestriction( 0, 1 ) ] ],
3382 'anon',
3383 true
3385 'ip-blocked with sysop performer' => [
3386 'ip',
3387 [ 'createAccount' => true ],
3388 'sysop',
3389 true
3391 'globally blocked' => [
3392 'global-ip',
3393 [ 'systemBlock' => 'test-systemBlock' ],
3394 'anon',
3395 false
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;
3412 return $req;
3414 $cmpReqs = static function ( $a, $b ) {
3415 $ret = strcmp( get_class( $a ), get_class( $b ) );
3416 if ( !$ret ) {
3417 $ret = strcmp( $a->getUniqueId(), $b->getUniqueId() );
3419 return $ret;
3422 $good = StatusValue::newGood();
3424 $mocks = [];
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' ) ];
3431 } );
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' ) ];
3440 } );
3441 $mocks[$key]->method( 'providerAllowsAuthenticationDataChange' )
3442 ->willReturn( $good );
3445 foreach ( [
3446 PrimaryAuthenticationProvider::TYPE_NONE,
3447 PrimaryAuthenticationProvider::TYPE_CREATE,
3448 PrimaryAuthenticationProvider::TYPE_LINK
3449 ] as $type ) {
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' ) ];
3459 } );
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' )
3471 ->willReturn( [] );
3472 $mocks['primary2']->method( 'providerAllowsAuthenticationDataChange' )
3473 ->willReturnCallback( static function ( $req ) use ( $good ) {
3474 return $req->getUniqueId() === 'generic' ? StatusValue::newFatal( 'no' ) : $good;
3475 } );
3476 $this->primaryauthMocks[] = $mocks['primary2'];
3478 $this->preauthMocks = [ $mocks['pre'] ];
3479 $this->secondaryauthMocks = [ $mocks['secondary'] ];
3480 $this->initializeManager( true );
3482 if ( $state ) {
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() {
3542 return [
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;
3614 return $req;
3616 $cmpReqs = static function ( $a, $b ) {
3617 $ret = strcmp( get_class( $a ), get_class( $b ) );
3618 if ( !$ret ) {
3619 $ret = strcmp( $a->getUniqueId(), $b->getUniqueId() );
3621 return $ret;
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 ) {
3631 return [
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 ),
3639 } );
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 ) {
3648 return [
3649 $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
3650 $makeReq( "required2", AuthenticationRequest::REQUIRED ),
3651 $makeReq( "optional2", AuthenticationRequest::OPTIONAL ),
3653 } );
3655 $secondary = $this->createMock( AbstractSecondaryAuthenticationProvider::class );
3656 $secondary->method( 'getUniqueId' )
3657 ->willReturn( 'secondary' );
3658 $secondary->method( 'getAuthenticationRequests' )
3659 ->willReturnCallback( static function ( $action ) use ( $makeReq ) {
3660 return [
3661 $makeReq( "foo", AuthenticationRequest::OPTIONAL ),
3662 $makeReq( "bar", AuthenticationRequest::REQUIRED ),
3663 $makeReq( "baz", AuthenticationRequest::REQUIRED ),
3665 } );
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 );
3675 $expected = [
3676 $rememberReq,
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 );
3695 $expected = [
3696 $rememberReq,
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() {
3710 $mocks = [];
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;
3719 } );
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();
3761 $session->clear();
3763 $this->assertSame( 0, User::newFromName( $username )->getId() );
3765 $callback = $this->callback( static function ( $user ) use ( $username ) {
3766 return $user->getName() === $username;
3767 } );
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();
3809 $session->clear();
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() {
3848 $types = [
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' );
3870 try {
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();
3900 $session = [
3901 'userid' => $user->getId(),
3902 'username' => $user->getName(),
3903 'primary' => 'X',
3906 try {
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 );
3937 try {
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 ) . '!',
3943 $ex->getMessage()
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 );
3961 $mocks = [];
3963 foreach ( [ 'pre', 'primary' ] as $key ) {
3964 $class = ucfirst( $key ) . 'AuthenticationProvider';
3965 $mocks[$key] = $this->getMockBuilder( "MediaWiki\\Auth\\Abstract$class" )
3966 ->setMockClassName( "MockAbstract$class" )
3967 ->getMock();
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" )
3974 ->getMock();
3975 $mocks[$key . $i]->method( 'getUniqueId' )
3976 ->willReturn( $key . $i );
3980 $mocks['pre']->method( 'testForAccountLink' )
3981 ->willReturnCallback(
3982 function ( $u )
3983 use ( $user, $preTest )
3985 $this->assertSame( $user->getId(), $u->getId() );
3986 $this->assertSame( $user->getName(), $u->getName() );
3987 return $preTest;
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() );
4000 $foundReq = false;
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 );
4007 } );
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;
4030 } );
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 );
4047 } );
4050 $first = true;
4051 $expectLog = [];
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' ];
4059 try {
4060 if ( $first ) {
4061 $ret = $this->manager->beginAccountLink( $user, [ $req ], 'http://localhost/' );
4062 } else {
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 ) {
4070 throw $ex;
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" );
4075 return;
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" );
4091 } else {
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" );
4110 $first = false;
4113 $this->assertSame( $expectLog, $this->logger->getBuffer() );
4116 public function provideAccountLink() {
4117 $req = $this->getMockForAbstractClass( AuthenticationRequest::class );
4118 $good = StatusValue::newGood();
4120 return [
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' => [
4129 $good,
4130 $tmp = [
4131 AuthenticationResponse::newFail( $this->message( 'fail-from-primary' ) ),
4133 $tmp
4135 'All primary abstain' => [
4136 $good,
4138 AuthenticationResponse::newAbstain(),
4141 AuthenticationResponse::newFail( $this->message( 'authmanager-link-no-primary' ) )
4144 'Primary UI, then redirect, then fail' => [
4145 $good,
4146 $tmp = [
4147 AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
4148 AuthenticationResponse::newRedirect( [ $req ], '/foo.html', [ 'foo' => 'bar' ] ),
4149 AuthenticationResponse::newFail( $this->message( 'fail-in-primary-continue' ) ),
4151 $tmp
4153 'Primary redirect, then abstain' => [
4154 $good,
4156 $tmp = AuthenticationResponse::newRedirect(
4157 [ $req ], '/foo.html', [ 'foo' => 'bar' ]
4159 AuthenticationResponse::newAbstain(),
4162 $tmp,
4163 new DomainException(
4164 'MockAbstractPrimaryAuthenticationProvider::continuePrimaryAccountLink() returned ABSTAIN'
4168 'Primary UI, then pass' => [
4169 $good,
4171 $tmp1 = AuthenticationResponse::newUI( [ $req ], $this->message( '...' ) ),
4172 AuthenticationResponse::newPass(),
4175 $tmp1,
4176 AuthenticationResponse::newPass( '' ),
4179 'Primary pass' => [
4180 $good,
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
4218 ): void {
4219 // Test the default primary and secondary authentication providers
4220 // irrespective of any potentially conflicting local configuration.
4221 $authConfig = $this->getServiceContainer()
4222 ->getConfigSchema()
4223 ->getDefaultFor( MainConfigNames::AuthManagerAutoConfig );
4225 $this->overrideConfigValues( [
4226 MainConfigNames::AuthManagerConfig => null,
4227 MainConfigNames::AuthManagerAutoConfig => $authConfig
4228 ] );
4230 $authManager = $this->getServiceContainer()->getAuthManager();
4231 $userProvider = $userProvider->bindTo( $this );
4233 $reqs = $authManager->getAuthenticationRequests( AuthManager::ACTION_CREATE, $userProvider() );
4235 $reqsByLevel = [];
4236 foreach ( $reqs as $req ) {
4237 $reqsByLevel[$req->required][] = get_class( $req );
4240 foreach ( $expectedReqsByLevel as $level => $expectedReqs ) {
4241 $reqs = $reqsByLevel[$level] ?? [];
4242 sort( $reqs );
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' => [
4264 function (): User {
4265 $req = new FauxRequest();
4266 return $this->getServiceContainer()
4267 ->getTempUserCreator()
4268 ->create( null, $req )
4269 ->getUser();
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
4295 // phpcs:enable