Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / auth / TemporaryPasswordPrimaryAuthenticationProviderTest.php
blob5baabb4b17e0b2a1ae960c9a8a4ff94c4337b4ce
1 <?php
3 namespace MediaWiki\Tests\Auth;
5 use MediaWiki\Auth\AuthenticationRequest;
6 use MediaWiki\Auth\AuthenticationResponse;
7 use MediaWiki\Auth\AuthManager;
8 use MediaWiki\Auth\PasswordAuthenticationRequest;
9 use MediaWiki\Auth\PrimaryAuthenticationProvider;
10 use MediaWiki\Auth\TemporaryPasswordAuthenticationRequest;
11 use MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider;
12 use MediaWiki\Config\HashConfig;
13 use MediaWiki\MainConfigNames;
14 use MediaWiki\Password\PasswordFactory;
15 use MediaWiki\Request\FauxRequest;
16 use MediaWiki\Status\Status;
17 use MediaWiki\Tests\Unit\Auth\AuthenticationProviderTestTrait;
18 use MediaWiki\Tests\Unit\DummyServicesTrait;
19 use MediaWiki\User\UserIdentity;
20 use MediaWiki\User\UserNameUtils;
21 use MediaWikiIntegrationTestCase;
22 use StatusValue;
23 use Wikimedia\Message\MessageSpecifier;
24 use Wikimedia\ScopedCallback;
25 use Wikimedia\TestingAccessWrapper;
27 /**
28 * TODO clean up and reduce duplication
30 * @group AuthManager
31 * @group Database
32 * @covers \MediaWiki\Auth\AbstractTemporaryPasswordPrimaryAuthenticationProvider
33 * @covers \MediaWiki\Auth\TemporaryPasswordPrimaryAuthenticationProvider
35 class TemporaryPasswordPrimaryAuthenticationProviderTest extends MediaWikiIntegrationTestCase {
36 use AuthenticationProviderTestTrait;
37 use DummyServicesTrait;
39 private AuthManager $manager;
40 private Status $validity;
42 private PasswordFactory $testPasswordFactory;
44 protected function setUp(): void {
45 parent::setUp();
47 $mwServices = $this->getServiceContainer();
49 $hookContainer = $this->createHookContainer();
51 $this->manager = new AuthManager(
52 new FauxRequest(),
53 $mwServices->getMainConfig(),
54 $this->getDummyObjectFactory(),
55 $hookContainer,
56 $mwServices->getReadOnlyMode(),
57 $this->createNoOpMock( UserNameUtils::class ),
58 $mwServices->getBlockManager(),
59 $mwServices->getWatchlistManager(),
60 $mwServices->getDBLoadBalancer(),
61 $mwServices->getContentLanguage(),
62 $mwServices->getLanguageConverterFactory(),
63 $mwServices->getBotPasswordStore(),
64 $mwServices->getUserFactory(),
65 $mwServices->getUserIdentityLookup(),
66 $mwServices->getUserOptionsManager()
69 $this->validity = Status::newGood();
71 // A is unsalted MD5 (thus fast) ... we don't care about security here, this is test only
72 $this->testPasswordFactory = new PasswordFactory(
73 $this->getConfVar( MainConfigNames::PasswordConfig ),
74 'A'
78 /**
79 * Get an instance of the provider
81 * $provider->checkPasswordValidity is mocked to return $this->validity,
82 * because we don't need to test that here.
84 * @param array $params
85 * @param UserNameUtils|null $userNameUtils
86 * @return TemporaryPasswordPrimaryAuthenticationProvider
88 protected function getProvider( array $params = [], ?UserNameUtils $userNameUtils = null ) {
89 $userNameUtils ??= $this->getServiceContainer()->getUserNameUtils();
90 $mwServices = $this->getServiceContainer();
92 $mockedMethods[] = 'checkPasswordValidity';
93 $provider = $this->getMockBuilder( TemporaryPasswordPrimaryAuthenticationProvider::class )
94 ->onlyMethods( $mockedMethods )
95 ->setConstructorArgs( [
96 $mwServices->getConnectionProvider(),
97 $mwServices->getUserOptionsLookup(),
98 $params,
99 ] )
100 ->getMock();
101 $provider->method( 'checkPasswordValidity' )
102 ->willReturnCallback( function () {
103 return $this->validity;
104 } );
105 $this->initProvider(
106 $provider, $mwServices->getMainConfig(), null, $this->manager, null, $userNameUtils
109 return $provider;
112 protected function hookMailer( $func = null ) {
113 $hookContainer = $this->getServiceContainer()->getHookContainer();
115 $this->clearHook( 'AlternateUserMailer' );
117 if ( $func ) {
118 $reset = $hookContainer->scopedRegister( 'AlternateUserMailer', $func );
119 } else {
120 $reset = $hookContainer->scopedRegister( 'AlternateUserMailer', function () {
121 $this->fail( 'AlternateUserMailer hook called unexpectedly' );
122 return false;
123 } );
125 return $reset;
129 * Set the new password (i.e. single use temporary password)
130 * hash for the given user, with an optional expiry time.
132 * @param UserIdentity $user The user to update the new password for.
133 * @param string $hash Password hash to store.
134 * @param int|null $expiry UNIX timestamp at which the new password expires, or `null` for no expiry.
136 private function setNewPassword(
137 UserIdentity $user,
138 string $hash,
139 ?int $expiry = null
140 ): void {
141 $dbw = $this->getDb();
142 $dbw->newUpdateQueryBuilder()
143 ->update( 'user' )
144 ->set( [
145 'user_newpassword' => $hash,
146 'user_newpass_time' => $expiry ? $dbw->timestamp( $expiry ) : null
148 ->where( [ 'user_id' => $user->getId() ] )
149 ->execute();
152 public function testBasics() {
153 $provider = $this->getProvider();
155 $this->assertSame(
156 PrimaryAuthenticationProvider::TYPE_CREATE,
157 $provider->accountCreationType()
160 $existingUserName = $this->getTestUser()->getUserIdentity()->getName();
161 $this->assertTrue( $provider->testUserExists( $existingUserName ) );
162 $this->assertTrue( $provider->testUserExists( lcfirst( $existingUserName ) ) );
163 $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
164 $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
166 $req = new PasswordAuthenticationRequest;
167 $req->action = AuthManager::ACTION_CHANGE;
168 $req->username = '<invalid>';
169 $provider->providerChangeAuthenticationData( $req );
172 public function testConfig() {
173 $config = new HashConfig( [
174 MainConfigNames::EnableEmail => false,
175 MainConfigNames::NewPasswordExpiry => 100,
176 MainConfigNames::PasswordReminderResendTime => 101,
177 ] );
179 $provider = new TemporaryPasswordPrimaryAuthenticationProvider(
180 $this->getServiceContainer()->getConnectionProvider(),
181 $this->getServiceContainer()->getUserOptionsLookup()
183 $providerPriv = TestingAccessWrapper::newFromObject( $provider );
184 $this->initProvider( $provider, $config );
185 $this->assertSame( false, $providerPriv->emailEnabled );
186 $this->assertSame( 100, $providerPriv->newPasswordExpiry );
187 $this->assertSame( 101, $providerPriv->passwordReminderResendTime );
189 $provider = new TemporaryPasswordPrimaryAuthenticationProvider(
190 $this->getServiceContainer()->getConnectionProvider(),
191 $this->getServiceContainer()->getUserOptionsLookup(),
193 'emailEnabled' => true,
194 'newPasswordExpiry' => 42,
195 'passwordReminderResendTime' => 43,
198 $providerPriv = TestingAccessWrapper::newFromObject( $provider );
199 $this->initProvider( $provider, $config );
200 $this->assertSame( true, $providerPriv->emailEnabled );
201 $this->assertSame( 42, $providerPriv->newPasswordExpiry );
202 $this->assertSame( 43, $providerPriv->passwordReminderResendTime );
206 * @dataProvider provideTestUserCanAuthenticateErrorCases
208 * @param string|null $userName The user name to check, or `null` to use the user name of the test user
209 * @param callable|null $passwordProvider Optional callable that takes a `PasswordFactory` and produces
210 * a password hash override to set for the test user
211 * @param int|null $passwordExpiry Expiry to set for the password returned by `$passwordProvider`, or
212 * `null` to set no expiry.
213 * @return void
215 public function testTestUserCanAuthenticateErrorCases(
216 ?string $userName = null,
217 ?callable $passwordProvider = null,
218 ?int $passwordExpiry = null
219 ): void {
220 $user = self::getMutableTestUser()->getUser();
222 if ( $passwordProvider !== null ) {
223 $this->setNewPassword(
224 $user,
225 $passwordProvider( $this->testPasswordFactory ),
226 $passwordExpiry
230 $userName ??= $user->getName();
232 $result = $this->getProvider( [ 'newPasswordExpiry' => 100 ] )->testUserCanAuthenticate( $userName );
234 $this->assertFalse( $result );
237 public function provideTestUserCanAuthenticateErrorCases(): iterable {
238 yield 'invalid user name' => [ '<invalid>' ];
239 yield 'nonexistent user' => [ 'DoesNotExist' ];
240 yield 'user with invalid password' => [
241 null,
242 fn () => PasswordFactory::newInvalidPassword()->toString()
244 yield 'user with expired password' => [
245 null,
246 fn ( PasswordFactory $passwordFactory ) => $passwordFactory->newFromPlaintext( 'password' )->toString(),
247 time() - 3_600
251 public function testTestUserCanAuthenticateSimple(): void {
252 $user = self::getMutableTestUser()->getUser();
254 $this->setNewPassword(
255 $user,
256 $this->testPasswordFactory->newFromPlaintext( 'password' )->toString()
259 $result = $this->getProvider()->testUserCanAuthenticate( $user->getName() );
261 $this->assertTrue( $result );
264 public function testTestUserCanAuthenticateCaseInsensitive(): void {
265 $user = self::getMutableTestUser()->getUser();
267 $this->setNewPassword(
268 $user,
269 $this->testPasswordFactory->newFromPlaintext( 'password' )->toString()
272 $result = $this->getProvider()->testUserCanAuthenticate( lcfirst( $user->getName() ) );
274 $this->assertTrue( $result );
277 public function testTestUserCanAuthenticateWithNonExpiredTemporaryPassword(): void {
278 $user = self::getMutableTestUser()->getUser();
280 $this->setNewPassword(
281 $user,
282 $this->testPasswordFactory->newFromPlaintext( 'password' )->toString(),
283 time() - 100
286 $result = $this->getProvider( [ 'newPasswordExpiry' => 3600 ] )->testUserCanAuthenticate( $user->getName() );
288 $this->assertTrue( $result );
292 * @dataProvider provideGetAuthenticationRequests
293 * @param string $action
294 * @param bool $registered
295 * @param bool $temporary
296 * @param AuthenticationRequest[] $expected
298 public function testGetAuthenticationRequests(
299 string $action,
300 bool $registered,
301 bool $temporary,
302 array $expected
304 $username = $registered ? 'TestGetAuthenticationRequests' : null;
305 $options = [ 'username' => $username ];
307 $userNameUtils = $this->createMock( UserNameUtils::class );
308 $userNameUtils->method( 'isTemp' )
309 ->with( $username )
310 ->willReturn( $temporary );
312 $actual = $this->getProvider( [ 'emailEnabled' => true ], $userNameUtils )
313 ->getAuthenticationRequests( $action, $options );
314 foreach ( $actual as $req ) {
315 if ( $req instanceof TemporaryPasswordAuthenticationRequest && $req->password !== null ) {
316 $req->password = 'random';
319 $this->assertEquals( $expected, $actual );
322 public static function provideGetAuthenticationRequests(): iterable {
323 yield 'login attempt as anonymous user' => [
324 AuthManager::ACTION_LOGIN, false, false, [ new PasswordAuthenticationRequest ]
327 yield 'login attempt as named user' => [
328 AuthManager::ACTION_LOGIN, true, false, [ new PasswordAuthenticationRequest ]
331 yield 'login attempt as temporary user' => [
332 AuthManager::ACTION_LOGIN, true, true, [ new PasswordAuthenticationRequest ]
335 yield 'signup attempt as anonymous user' => [
336 AuthManager::ACTION_CREATE, false, false, []
339 yield 'signup attempt as named user' => [
340 AuthManager::ACTION_CREATE, true, false, [ new TemporaryPasswordAuthenticationRequest( 'random' ) ]
343 yield 'signup attempt as temporary user' => [
344 AuthManager::ACTION_CREATE, true, true, []
347 yield 'account linking attempt as anonymous user' => [
348 AuthManager::ACTION_LINK, false, false, []
351 yield 'account linking attempt as named user' => [
352 AuthManager::ACTION_LINK, true, false, []
355 yield 'account linking attempt as temporary user' => [
356 AuthManager::ACTION_LINK, true, true, []
359 yield 'credential change attempt as anonymous user' => [
360 AuthManager::ACTION_CHANGE, false, false, [ new TemporaryPasswordAuthenticationRequest( 'random' ) ]
363 yield 'credential change attempt as named user' => [
364 AuthManager::ACTION_CHANGE, true, false, [ new TemporaryPasswordAuthenticationRequest( 'random' ) ]
367 yield 'credential change attempt as temporary user' => [
368 AuthManager::ACTION_CHANGE, true, true, [ new TemporaryPasswordAuthenticationRequest( 'random' ) ]
371 yield 'credential remove attempt as anonymous user' => [
372 AuthManager::ACTION_REMOVE, false, false, [ new TemporaryPasswordAuthenticationRequest() ]
375 yield 'credential remove attempt as named user' => [
376 AuthManager::ACTION_REMOVE, true, false, [ new TemporaryPasswordAuthenticationRequest() ]
379 yield 'credential remove attempt as temporary user' => [
380 AuthManager::ACTION_REMOVE, true, true, [ new TemporaryPasswordAuthenticationRequest() ]
385 * @dataProvider provideAuthenticationErrorCases
386 * @param string $password
387 * @param string $expectedErrorMessage
388 * @param int $newPasswordExpiry
389 * @param StatusValue|null $validationError
390 * @return void
392 public function testAuthenticationErrorCases(
393 string $password,
394 string $expectedErrorMessage,
395 int $newPasswordExpiry = 100,
396 ?StatusValue $validationError = null
398 $user = self::getMutableTestUser()->getUser();
400 $validPassword = 'TemporaryPassword';
401 $hash = ':A:' . md5( $validPassword );
403 $this->setNewPassword( $user, $hash, time() - 10 );
405 $req = self::makePasswordAuthenticationRequest( $user->getName(), $password );
407 $reqs = [ PasswordAuthenticationRequest::class => $req ];
409 $provider = $this->getProvider( [ 'newPasswordExpiry' => $newPasswordExpiry ] );
411 $this->validity = $validationError ?? Status::newGood();
413 $response = $provider->beginPrimaryAuthentication( $reqs );
415 $this->assertSame( AuthenticationResponse::FAIL, $response->status );
416 if ( $validationError !== null ) {
417 $this->assertSame(
418 $validationError->getMessages()[0]->getKey(),
419 $response->message->getParams()[0]->getKey()
424 public static function provideAuthenticationErrorCases(): iterable {
425 yield 'validation failure' => [
426 'TemporaryPassword',
427 'fatalpassworderror',
428 100,
429 Status::newFatal( 'arbitrary-failure' )
432 yield 'expired password' => [
433 'TemporaryPassword',
434 'wrongpassword',
438 yield 'wrong password' => [
439 'Wrong',
440 'wrongpassword'
445 * @dataProvider provideAuthenticationAbstainCases
446 * @param PasswordAuthenticationRequest|null $req The authentication request to send,
447 * or `null` to send no requests
448 * @return void
450 public function testAuthenticationAbstainCases( ?PasswordAuthenticationRequest $req ): void {
451 $reqs = $req ? [ PasswordAuthenticationRequest::class => $req ] : [];
453 $response = $this->getProvider()->beginPrimaryAuthentication( $reqs );
455 $this->assertEquals( AuthenticationResponse::newAbstain(), $response );
458 public static function provideAuthenticationAbstainCases(): iterable {
459 yield 'no requests' => [ null ];
460 yield 'no user name' => [ self::makePasswordAuthenticationRequest( null, 'bar' ) ];
461 yield 'no password' => [ self::makePasswordAuthenticationRequest( 'foo' ) ];
462 yield 'invalid user name' => [ self::makePasswordAuthenticationRequest( '<invalid>', 'bar' ) ];
463 yield 'nonexistent user' => [ self::makePasswordAuthenticationRequest( 'DoesNotExist', 'bar' ) ];
466 private static function makePasswordAuthenticationRequest(
467 ?string $userName = null,
468 ?string $password = null
469 ): PasswordAuthenticationRequest {
470 $req = new PasswordAuthenticationRequest();
471 $req->action = AuthManager::ACTION_LOGIN;
472 $req->username = $userName;
473 $req->password = $password;
474 return $req;
477 public function testAuthenticationSuccess(): void {
478 $user = self::getMutableTestUser()->getUser();
480 $password = 'TemporaryPassword';
481 $hash = ':A:' . md5( $password );
483 $this->setNewPassword( $user, $hash, time() - 10 );
485 $req = self::makePasswordAuthenticationRequest( $user->getName(), $password );
486 $reqs = [ PasswordAuthenticationRequest::class => $req ];
488 $provider = $this->getProvider();
490 $this->manager->removeAuthenticationSessionData( null );
491 $this->validity = Status::newGood();
493 $this->assertEquals(
494 AuthenticationResponse::newPass( $user->getName() ),
495 $provider->beginPrimaryAuthentication( $reqs )
498 $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
501 public function testAuthenticationSuccessCaseInsensitive(): void {
502 $user = self::getMutableTestUser()->getUser();
504 $password = 'TemporaryPassword';
505 $hash = ':A:' . md5( $password );
507 $this->setNewPassword( $user, $hash, time() - 10 );
509 $req = self::makePasswordAuthenticationRequest( lcfirst( $user->getName() ), $password );
510 $reqs = [ PasswordAuthenticationRequest::class => $req ];
512 $provider = $this->getProvider();
514 $this->manager->removeAuthenticationSessionData( null );
515 $this->validity = Status::newGood();
517 $this->assertEquals(
518 AuthenticationResponse::newPass( $user->getName() ),
519 $provider->beginPrimaryAuthentication( $reqs )
522 $this->assertNotNull( $this->manager->getAuthenticationSessionData( 'reset-pass' ) );
526 * @dataProvider provideProviderAllowsAuthenticationDataChange
528 * @param string $type
529 * @param callable $usernameGetter Function that takes the username of a sysop user and returns the username to
530 * use for testing.
531 * @param Status $validity Result of the password validity check
532 * @param StatusValue $expect1 Expected result with $checkData = false
533 * @param StatusValue $expect2 Expected result with $checkData = true
535 public function testProviderAllowsAuthenticationDataChange( $type, callable $usernameGetter,
536 Status $validity,
537 StatusValue $expect1, StatusValue $expect2
539 $user = $usernameGetter( $this->getTestSysop()->getUserIdentity()->getName() );
540 if ( $type === PasswordAuthenticationRequest::class ||
541 $type === TemporaryPasswordAuthenticationRequest::class
543 $req = new $type();
544 $req->password = 'NewPassword';
545 } else {
546 $req = $this->createMock( $type );
548 $req->action = AuthManager::ACTION_CHANGE;
549 $req->username = $user;
551 $provider = $this->getProvider();
552 $this->validity = $validity;
553 $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
554 $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
557 public static function provideProviderAllowsAuthenticationDataChange() {
558 $err = StatusValue::newGood();
559 $err->error( 'arbitrary-warning' );
561 return [
563 AuthenticationRequest::class,
564 static fn ( $sysopUsername ) => $sysopUsername,
565 Status::newGood(),
566 StatusValue::newGood( 'ignored' ),
567 StatusValue::newGood( 'ignored' ),
570 PasswordAuthenticationRequest::class,
571 static fn ( $sysopUsername ) => $sysopUsername,
572 Status::newGood(),
573 StatusValue::newGood( 'ignored' ),
574 StatusValue::newGood( 'ignored' ),
577 TemporaryPasswordAuthenticationRequest::class,
578 static fn ( $sysopUsername ) => $sysopUsername,
579 Status::newGood(),
580 StatusValue::newGood(),
581 StatusValue::newGood(),
584 TemporaryPasswordAuthenticationRequest::class,
585 'lcfirst',
586 Status::newGood(),
587 StatusValue::newGood(),
588 StatusValue::newGood(),
591 TemporaryPasswordAuthenticationRequest::class,
592 static fn ( $sysopUsername ) => $sysopUsername,
593 Status::wrap( $err ),
594 StatusValue::newGood(),
595 $err,
598 TemporaryPasswordAuthenticationRequest::class,
599 static fn ( $sysopUsername ) => $sysopUsername,
600 Status::newFatal( 'arbitrary-error' ),
601 StatusValue::newGood(),
602 StatusValue::newFatal( 'arbitrary-error' ),
605 TemporaryPasswordAuthenticationRequest::class,
606 static fn () => 'DoesNotExist',
607 Status::newGood(),
608 StatusValue::newGood(),
609 StatusValue::newGood( 'ignored' ),
612 TemporaryPasswordAuthenticationRequest::class,
613 static fn () => '<invalid>',
614 Status::newGood(),
615 StatusValue::newGood(),
616 StatusValue::newGood( 'ignored' ),
622 * @dataProvider provideProviderChangeAuthenticationData
623 * @param string $type
624 * @param bool $changed
626 public function testProviderChangeAuthenticationData( $type, $changed ) {
627 $user = $this->getTestSysop()->getUserIdentity()->getName();
628 $oldpass = 'OldTempPassword';
629 $newpass = 'NewTempPassword';
631 $dbw = $this->getDb();
632 $oldHash = $dbw->newSelectQueryBuilder()
633 ->select( 'user_newpassword' )
634 ->from( 'user' )
635 ->where( [ 'user_name' => $user ] )
636 ->fetchField();
637 $cb = new ScopedCallback( static function () use ( $dbw, $user, $oldHash ) {
638 $dbw->newUpdateQueryBuilder()
639 ->update( 'user' )
640 ->set( [ 'user_newpassword' => $oldHash ] )
641 ->where( [ 'user_name' => $user ] )
642 ->execute();
643 } );
645 $hash = ':A:' . md5( $oldpass );
646 $dbw->newUpdateQueryBuilder()
647 ->update( 'user' )
648 ->set( [ 'user_newpassword' => $hash, 'user_newpass_time' => $dbw->timestamp( time() + 1000 ) ] )
649 ->where( [ 'user_name' => $user ] )
650 ->execute();
652 $provider = $this->getProvider();
654 $loginReq = new PasswordAuthenticationRequest();
655 $loginReq->action = AuthManager::ACTION_CHANGE;
656 $loginReq->username = $user;
657 $loginReq->password = $oldpass;
658 $loginReqs = [ PasswordAuthenticationRequest::class => $loginReq ];
659 $this->assertEquals(
660 AuthenticationResponse::newPass( $user ),
661 $provider->beginPrimaryAuthentication( $loginReqs )
664 if ( $type === PasswordAuthenticationRequest::class ||
665 $type === TemporaryPasswordAuthenticationRequest::class
667 $changeReq = new $type();
668 $changeReq->password = $newpass;
669 } else {
670 $changeReq = $this->createMock( $type );
672 $changeReq->action = AuthManager::ACTION_CHANGE;
673 $changeReq->username = $user;
674 $resetMailer = $this->hookMailer();
675 $provider->providerChangeAuthenticationData( $changeReq );
676 ScopedCallback::consume( $resetMailer );
678 $loginReq->password = $oldpass;
679 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
680 $this->assertEquals(
681 AuthenticationResponse::FAIL,
682 $ret->status,
683 'old password should fail'
685 $this->assertEquals(
686 'wrongpassword',
687 $ret->message->getKey(),
688 'old password should fail'
691 $loginReq->password = $newpass;
692 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
693 if ( $changed ) {
694 $this->assertEquals(
695 AuthenticationResponse::newPass( $user ),
696 $ret,
697 'new password should pass'
699 $this->assertNotNull(
700 $dbw->newSelectQueryBuilder()
701 ->select( 'user_newpass_time' )
702 ->from( 'user' )
703 ->where( [ 'user_name' => $user ] )
704 ->fetchField()
706 } else {
707 $this->assertEquals(
708 AuthenticationResponse::FAIL,
709 $ret->status,
710 'new password should fail'
712 $this->assertEquals(
713 'wrongpassword',
714 $ret->message->getKey(),
715 'new password should fail'
717 $this->assertNull(
718 $dbw->newSelectQueryBuilder()
719 ->select( 'user_newpass_time' )
720 ->from( 'user' )
721 ->where( [ 'user_name' => $user ] )
722 ->fetchField()
727 public static function provideProviderChangeAuthenticationData() {
728 return [
729 [ AuthenticationRequest::class, false ],
730 [ PasswordAuthenticationRequest::class, false ],
731 [ TemporaryPasswordAuthenticationRequest::class, true ],
736 * @dataProvider provideChangeAuthenticationDataEmailErrorCases
738 * @param array $providerConfig Configuration to pass on to the auth provider
739 * @param string|null $caller Caller on behalf of which the request is sent
740 * @param string $expectedError Expected error message key
742 public function testProviderChangeAuthenticationDataEmailError(
743 array $providerConfig,
744 ?string $caller,
745 string $expectedError
746 ): void {
747 $user = self::getMutableTestUser()->getUser();
749 $dbw = $this->getDb();
750 $dbw->newUpdateQueryBuilder()
751 ->update( 'user' )
752 ->set( [ 'user_newpass_time' => $dbw->timestamp( time() - 5 * 3600 ) ] )
753 ->where( [ 'user_id' => $user->getId() ] )
754 ->execute();
756 $req = TemporaryPasswordAuthenticationRequest::newRandom();
757 $req->username = $user->getName();
758 $req->mailpassword = true;
759 $req->caller = $caller;
761 $provider = $this->getProvider( $providerConfig );
762 $status = $provider->providerAllowsAuthenticationDataChange( $req );
764 $this->assertFalse( $status->isGood() );
765 $this->assertSame(
766 [ $expectedError ],
767 array_map( fn ( MessageSpecifier $spec ) => $spec->getKey(), $status->getMessages() )
771 public static function provideChangeAuthenticationDataEmailErrorCases(): iterable {
772 yield 'email disabled' => [
773 [ 'emailEnabled' => false ],
774 '127.0.0.1',
775 'passwordreset-emaildisabled'
778 yield 'password reset rate limited' => [
779 [ 'emailEnabled' => true, 'passwordReminderResendTime' => 10 ],
780 '127.0.0.1',
781 'throttled-mailpassword'
784 yield 'missing caller' => [
785 [ 'emailEnabled' => true, 'passwordReminderResendTime' => 0 ],
786 null,
787 'passwordreset-nocaller'
790 yield 'invalid IP caller' => [
791 [ 'emailEnabled' => true, 'passwordReminderResendTime' => 0 ],
792 '127.0.0.256',
793 'passwordreset-nosuchcaller'
796 yield 'invalid registered caller' => [
797 [ 'emailEnabled' => true, 'passwordReminderResendTime' => 0 ],
798 '<Invalid>',
799 'passwordreset-nosuchcaller'
804 * @dataProvider provideChangeAuthenticationDataEmailSuccessCases
805 * @param string $caller Caller on behalf of which the request is sent
807 public function testProviderChangeAuthenticationDataEmailSuccess( string $caller ) {
808 $user = self::getMutableTestUser()->getUser();
810 $dbw = $this->getDb();
811 $dbw->newUpdateQueryBuilder()
812 ->update( 'user' )
813 ->set( [ 'user_newpass_time' => $dbw->timestamp( time() + 5 * 3600 ) ] )
814 ->where( [ 'user_id' => $user->getId() ] )
815 ->execute();
817 $req = TemporaryPasswordAuthenticationRequest::newRandom();
818 $req->username = $user->getName();
819 $req->mailpassword = true;
820 $req->caller = $caller;
822 $provider = $this->getProvider( [ 'emailEnabled' => true, 'passwordReminderResendTime' => 0 ] );
824 $status = $provider->providerAllowsAuthenticationDataChange( $req, true );
825 $this->assertEquals( StatusValue::newGood(), $status );
827 $mailed = false;
828 $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
829 use ( &$mailed, $req, $user )
831 $mailed = true;
832 $this->assertSame( $user->getEmail(), $to[0]->address );
833 $this->assertStringContainsString( $req->password, $body );
834 return false;
835 } );
836 $provider->providerChangeAuthenticationData( $req );
837 ScopedCallback::consume( $resetMailer );
838 $this->assertTrue( $mailed );
841 public static function provideChangeAuthenticationDataEmailSuccessCases(): iterable {
842 yield 'anonymous caller' => [ '127.0.0.1' ];
843 yield 'registered caller' => [ 'TestUser' ];
847 * @dataProvider provideAccountCreationSuccessCases
848 * @param AuthenticationRequest[] $reqs
850 public function testTestForAccountCreationSuccess( array $reqs ) {
851 $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'foo' );
853 $status = $this->getProvider()->testForAccountCreation( $user, $user, $reqs );
855 $this->assertTrue( $status->isGood() );
858 public static function provideAccountCreationSuccessCases(): iterable {
859 $req = new TemporaryPasswordAuthenticationRequest();
860 $req->username = 'Foo';
861 $req->password = 'Bar';
863 yield 'no password request' => [
867 yield 'validated password request' => [
868 [ TemporaryPasswordAuthenticationRequest::class => $req ],
872 public function testTestForAccountCreationError(): void {
873 $req = new TemporaryPasswordAuthenticationRequest();
874 $req->username = 'Foo';
875 $req->password = 'Bar';
877 $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'foo' );
878 $provider = $this->getProvider();
879 $this->validity->error( 'arbitrary warning' );
881 $status = $provider->testForAccountCreation(
882 $user, $user, [ TemporaryPasswordAuthenticationRequest::class => $req ]
885 $this->assertFalse( $status->isGood() );
886 $this->assertTrue( $status->hasMessage( 'arbitrary warning' ) );
890 * @dataProvider provideAccountCreationAbstainCases
891 * @param TemporaryPasswordAuthenticationRequest|null $req
892 * @return void
894 public function testAccountCreationAbstain( ?TemporaryPasswordAuthenticationRequest $req ) {
895 $resetMailer = $this->hookMailer();
897 $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'Foo' );
899 $reqs = $req ? [ TemporaryPasswordAuthenticationRequest::class => $req ] : [];
901 $provider = $this->getProvider();
902 $response = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
904 $this->assertSame( AuthenticationResponse::ABSTAIN, $response->status );
907 public static function provideAccountCreationAbstainCases(): iterable {
908 yield 'no authentication requests' => [
909 null,
912 yield 'request without password' => [
913 self::makeTemporaryPasswordAuthenticationRequest( 'foo' ),
916 yield 'request without username' => [
917 self::makeTemporaryPasswordAuthenticationRequest( null, 'bar' ),
921 public function testAccountCreationPassForUserNameWithDifferentCase(): void {
922 $user = $this->getServiceContainer()->getUserFactory()->newFromName( 'Foo' );
923 $pass = 'NewPassword';
925 $req = self::makeTemporaryPasswordAuthenticationRequest( 'foo', $pass );
926 $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
928 $provider = $this->getProvider();
929 $response = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
931 $this->assertSame( AuthenticationResponse::PASS, $response->status );
932 $this->assertSame( $response->username, $user->getName() );
933 $this->assertSame(
934 $response->createRequest->username,
935 $user->getName()
939 public function testAccountCreationPass(): void {
940 $resetMailer = $this->hookMailer();
942 $user = self::getMutableTestUser()->getUser();
943 $pass = 'NewPassword';
945 $req = self::makeTemporaryPasswordAuthenticationRequest( $user->getName(), $pass );
946 $reqs = [ TemporaryPasswordAuthenticationRequest::class => $req ];
948 $provider = $this->getProvider();
949 $response = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
951 $this->assertSame( AuthenticationResponse::PASS, $response->status );
952 $this->assertSame( $response->username, $user->getName() );
953 $this->assertSame(
954 $response->createRequest->username,
955 $user->getName()
957 $this->assertNull( $this->manager->getAuthenticationSessionData( 'no-email' ) );
959 $authreq = new PasswordAuthenticationRequest();
960 $authreq->action = AuthManager::ACTION_CREATE;
961 $authreq->username = $user->getName();
962 $authreq->password = $pass;
964 $authreqs = [ PasswordAuthenticationRequest::class => $authreq ];
966 $failedAttemptResponse = $provider->beginPrimaryAuthentication( $authreqs );
967 $this->assertSame( AuthenticationResponse::FAIL, $failedAttemptResponse->status, 'account creation not finished yet' );
969 $this->assertSame( null, $provider->finishAccountCreation( $user, $user, $response ) );
971 $response = $provider->beginPrimaryAuthentication( $authreqs );
972 $this->assertSame( AuthenticationResponse::PASS, $response->status, 'new password is set' );
975 private static function makeTemporaryPasswordAuthenticationRequest(
976 ?string $userName = null,
977 ?string $password = null
978 ): TemporaryPasswordAuthenticationRequest {
979 $req = new TemporaryPasswordAuthenticationRequest();
980 $req->username = $userName;
981 $req->password = $password;
982 return $req;
986 * @dataProvider provideAccountCreationEmailErrorCases
988 * @param array $providerConfig Configuration to pass on to the auth provider
989 * @param string $userEmail Email to set for the user being tested
990 * @param string $expectedError Expected error message key
992 public function testAccountCreationEmailErrorCases(
993 array $providerConfig,
994 string $userEmail,
995 string $expectedError
996 ): void {
997 $creator = $this->getServiceContainer()->getUserFactory()->newFromName( 'Foo' );
999 $user = self::getMutableTestUser()->getUser();
1000 $user->setEmail( $userEmail );
1002 $req = TemporaryPasswordAuthenticationRequest::newRandom();
1003 $req->username = $user->getName();
1004 $req->mailpassword = true;
1006 $provider = $this->getProvider( $providerConfig );
1007 $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
1008 $this->assertEquals( StatusValue::newFatal( $expectedError ), $status );
1011 public static function provideAccountCreationEmailErrorCases(): iterable {
1012 yield 'email disabled' => [
1013 [ 'emailEnabled' => false ],
1014 'test@localhost.localdomain',
1015 'emaildisabled'
1018 yield 'missing user email' => [
1019 [ 'emailEnabled' => true ],
1021 'noemailcreate'
1025 public function testAccountCreationEmailSuccess(): void {
1026 $creator = $this->getServiceContainer()->getUserFactory()->newFromName( 'Foo' );
1028 $user = self::getMutableTestUser()->getUser();
1029 $user->setEmail( 'test@localhost.localdomain' );
1031 $req = TemporaryPasswordAuthenticationRequest::newRandom();
1032 $req->username = $user->getName();
1033 $req->mailpassword = true;
1035 $provider = $this->getProvider( [ 'emailEnabled' => true ] );
1036 $status = $provider->testForAccountCreation( $user, $creator, [ $req ] );
1037 $this->assertEquals( StatusValue::newGood(), $status );
1039 $mailed = false;
1040 $resetMailer = $this->hookMailer( function ( $headers, $to, $from, $subject, $body )
1041 use ( &$mailed, $req )
1043 $mailed = true;
1044 $this->assertSame( 'test@localhost.localdomain', $to[0]->address );
1045 $this->assertStringContainsString( $req->password, $body );
1046 return false;
1047 } );
1049 $expect = AuthenticationResponse::newPass( $user->getName() );
1050 $expect->createRequest = clone $req;
1051 $expect->createRequest->username = $user->getName();
1052 $res = $provider->beginPrimaryAccountCreation( $user, $creator, [ $req ] );
1053 $this->assertEquals( $expect, $res );
1054 $this->assertTrue( $this->manager->getAuthenticationSessionData( 'no-email' ) );
1055 $this->assertFalse( $mailed );
1057 $this->assertSame( 'byemail', $provider->finishAccountCreation( $user, $creator, $res ) );
1058 $this->assertTrue( $mailed );
1060 ScopedCallback::consume( $resetMailer );
1061 $this->assertTrue( $mailed );