3 namespace MediaWiki\Auth
;
8 * @covers MediaWiki\Auth\LocalPasswordPrimaryAuthenticationProvider
10 class LocalPasswordPrimaryAuthenticationProviderTest
extends \MediaWikiTestCase
{
12 private $manager = null;
13 private $config = null;
14 private $validity = null;
16 protected function setUp() {
17 global $wgDisableAuthManager;
20 if ( $wgDisableAuthManager ) {
21 $this->markTestSkipped( '$wgDisableAuthManager is set' );
26 * Get an instance of the provider
28 * $provider->checkPasswordValidity is mocked to return $this->validity,
29 * because we don't need to test that here.
31 * @param bool $loginOnly
32 * @return LocalPasswordPrimaryAuthenticationProvider
34 protected function getProvider( $loginOnly = false ) {
35 if ( !$this->config
) {
36 $this->config
= new \
HashConfig();
38 $config = new \
MultiConfig( [
40 \ConfigFactory
::getDefaultInstance()->makeConfig( 'main' )
43 if ( !$this->manager
) {
44 $this->manager
= new AuthManager( new \
FauxRequest(), $config );
46 $this->validity
= \Status
::newGood();
48 $provider = $this->getMock(
49 LocalPasswordPrimaryAuthenticationProvider
::class,
50 [ 'checkPasswordValidity' ],
51 [ [ 'loginOnly' => $loginOnly ] ]
53 $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
54 ->will( $this->returnCallback( function () {
55 return $this->validity
;
57 $provider->setConfig( $config );
58 $provider->setLogger( new \Psr\Log\
NullLogger() );
59 $provider->setManager( $this->manager
);
64 public function testBasics() {
65 $provider = new LocalPasswordPrimaryAuthenticationProvider();
68 PrimaryAuthenticationProvider
::TYPE_CREATE
,
69 $provider->accountCreationType()
72 $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
73 $this->assertTrue( $provider->testUserExists( 'uTSysop' ) );
74 $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
75 $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
77 $provider = new LocalPasswordPrimaryAuthenticationProvider( [ 'loginOnly' => true ] );
80 PrimaryAuthenticationProvider
::TYPE_NONE
,
81 $provider->accountCreationType()
84 $this->assertTrue( $provider->testUserExists( 'UTSysop' ) );
85 $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
87 $req = new PasswordAuthenticationRequest
;
88 $req->action
= AuthManager
::ACTION_CHANGE
;
89 $req->username
= '<invalid>';
90 $provider->providerChangeAuthenticationData( $req );
93 public function testTestUserCanAuthenticate() {
94 $dbw = wfGetDB( DB_MASTER
);
95 $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => 'UTSysop' ] );
96 $cb = new \
ScopedCallback( function () use ( $dbw, $oldHash ) {
97 $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => 'UTSysop' ] );
99 $id = \User
::idFromName( 'UTSysop' );
101 $provider = $this->getProvider();
103 $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
105 $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
107 $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
108 $this->assertTrue( $provider->testUserCanAuthenticate( 'uTSysop' ) );
112 [ 'user_password' => \PasswordFactory
::newInvalidPassword()->toString() ],
113 [ 'user_name' => 'UTSysop' ]
115 $this->assertFalse( $provider->testUserCanAuthenticate( 'UTSysop' ) );
120 [ 'user_password' => '0123456789abcdef0123456789abcdef' ],
121 [ 'user_name' => 'UTSysop' ]
123 $this->assertTrue( $provider->testUserCanAuthenticate( 'UTSysop' ) );
126 public function testSetPasswordResetFlag() {
128 $this->getProvider();
130 /// @todo: Because we're currently using User, which uses the global config...
131 $this->setMwGlobals( [ 'wgPasswordExpireGrace' => 100 ] );
133 $this->config
->set( 'PasswordExpireGrace', 100 );
134 $this->config
->set( 'InvalidPasswordReset', true );
136 $provider = new LocalPasswordPrimaryAuthenticationProvider();
137 $provider->setConfig( $this->config
);
138 $provider->setLogger( new \Psr\Log\
NullLogger() );
139 $provider->setManager( $this->manager
);
140 $providerPriv = \TestingAccessWrapper
::newFromObject( $provider );
142 $dbw = wfGetDB( DB_MASTER
);
143 $row = $dbw->selectRow(
146 [ 'user_name' => 'UTSysop' ],
150 $this->manager
->removeAuthenticationSessionData( null );
151 $row->user_password_expires
= wfTimestamp( TS_MW
, time() +
200 );
152 $providerPriv->setPasswordResetFlag( 'UTSysop', \Status
::newGood(), $row );
153 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
155 $this->manager
->removeAuthenticationSessionData( null );
156 $row->user_password_expires
= wfTimestamp( TS_MW
, time() - 200 );
157 $providerPriv->setPasswordResetFlag( 'UTSysop', \Status
::newGood(), $row );
158 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
159 $this->assertNotNull( $ret );
160 $this->assertSame( 'resetpass-expired', $ret->msg
->getKey() );
161 $this->assertTrue( $ret->hard
);
163 $this->manager
->removeAuthenticationSessionData( null );
164 $row->user_password_expires
= wfTimestamp( TS_MW
, time() - 1 );
165 $providerPriv->setPasswordResetFlag( 'UTSysop', \Status
::newGood(), $row );
166 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
167 $this->assertNotNull( $ret );
168 $this->assertSame( 'resetpass-expired-soft', $ret->msg
->getKey() );
169 $this->assertFalse( $ret->hard
);
171 $this->manager
->removeAuthenticationSessionData( null );
172 $row->user_password_expires
= null;
173 $status = \Status
::newGood();
174 $status->error( 'testing' );
175 $providerPriv->setPasswordResetFlag( 'UTSysop', $status, $row );
176 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
177 $this->assertNotNull( $ret );
178 $this->assertSame( 'resetpass-validity-soft', $ret->msg
->getKey() );
179 $this->assertFalse( $ret->hard
);
182 public function testAuthentication() {
183 $dbw = wfGetDB( DB_MASTER
);
184 $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => 'UTSysop' ] );
185 $cb = new \
ScopedCallback( function () use ( $dbw, $oldHash ) {
186 $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => 'UTSysop' ] );
188 $id = \User
::idFromName( 'UTSysop' );
190 $req = new PasswordAuthenticationRequest();
191 $req->action
= AuthManager
::ACTION_LOGIN
;
192 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
194 $provider = $this->getProvider();
198 AuthenticationResponse
::newAbstain(),
199 $provider->beginPrimaryAuthentication( [] )
202 $req->username
= 'foo';
203 $req->password
= null;
205 AuthenticationResponse
::newAbstain(),
206 $provider->beginPrimaryAuthentication( $reqs )
209 $req->username
= null;
210 $req->password
= 'bar';
212 AuthenticationResponse
::newAbstain(),
213 $provider->beginPrimaryAuthentication( $reqs )
216 $req->username
= '<invalid>';
217 $req->password
= 'WhoCares';
218 $ret = $provider->beginPrimaryAuthentication( $reqs );
220 AuthenticationResponse
::newAbstain(),
221 $provider->beginPrimaryAuthentication( $reqs )
224 $req->username
= 'DoesNotExist';
225 $req->password
= 'DoesNotExist';
226 $ret = $provider->beginPrimaryAuthentication( $reqs );
228 AuthenticationResponse
::newAbstain(),
229 $provider->beginPrimaryAuthentication( $reqs )
232 // Validation failure
233 $req->username
= 'UTSysop';
234 $req->password
= 'UTSysopPassword';
235 $this->validity
= \Status
::newFatal( 'arbitrary-failure' );
236 $ret = $provider->beginPrimaryAuthentication( $reqs );
238 AuthenticationResponse
::FAIL
,
243 $ret->message
->getKey()
247 $this->manager
->removeAuthenticationSessionData( null );
248 $this->validity
= \Status
::newGood();
250 AuthenticationResponse
::newPass( 'UTSysop' ),
251 $provider->beginPrimaryAuthentication( $reqs )
253 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
255 // Successful auth after normalizing name
256 $this->manager
->removeAuthenticationSessionData( null );
257 $this->validity
= \Status
::newGood();
258 $req->username
= 'uTSysop';
260 AuthenticationResponse
::newPass( 'UTSysop' ),
261 $provider->beginPrimaryAuthentication( $reqs )
263 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
264 $req->username
= 'UTSysop';
266 // Successful auth with reset
267 $this->manager
->removeAuthenticationSessionData( null );
268 $this->validity
->error( 'arbitrary-warning' );
270 AuthenticationResponse
::newPass( 'UTSysop' ),
271 $provider->beginPrimaryAuthentication( $reqs )
273 $this->assertNotNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
276 $this->validity
= \Status
::newGood();
277 $req->password
= 'Wrong';
278 $ret = $provider->beginPrimaryAuthentication( $reqs );
280 AuthenticationResponse
::FAIL
,
285 $ret->message
->getKey()
288 // Correct handling of legacy encodings
289 $password = ':B:salt:' . md5( 'salt-' . md5( "\xe1\xe9\xed\xf3\xfa" ) );
290 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
291 $req->password
= 'áéÃóú';
292 $ret = $provider->beginPrimaryAuthentication( $reqs );
294 AuthenticationResponse
::FAIL
,
299 $ret->message
->getKey()
302 $this->config
->set( 'LegacyEncoding', true );
304 AuthenticationResponse
::newPass( 'UTSysop' ),
305 $provider->beginPrimaryAuthentication( $reqs )
308 $req->password
= 'áéÃóú Wrong';
309 $ret = $provider->beginPrimaryAuthentication( $reqs );
311 AuthenticationResponse
::FAIL
,
316 $ret->message
->getKey()
319 // Correct handling of really old password hashes
320 $this->config
->set( 'PasswordSalt', false );
321 $password = md5( 'FooBar' );
322 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
323 $req->password
= 'FooBar';
325 AuthenticationResponse
::newPass( 'UTSysop' ),
326 $provider->beginPrimaryAuthentication( $reqs )
329 $this->config
->set( 'PasswordSalt', true );
330 $password = md5( "$id-" . md5( 'FooBar' ) );
331 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => 'UTSysop' ] );
332 $req->password
= 'FooBar';
334 AuthenticationResponse
::newPass( 'UTSysop' ),
335 $provider->beginPrimaryAuthentication( $reqs )
341 * @dataProvider provideProviderAllowsAuthenticationDataChange
342 * @param string $type
343 * @param string $user
344 * @param \Status $validity Result of the password validity check
345 * @param \StatusValue $expect1 Expected result with $checkData = false
346 * @param \StatusValue $expect2 Expected result with $checkData = true
348 public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status
$validity,
349 \StatusValue
$expect1, \StatusValue
$expect2
351 if ( $type === PasswordAuthenticationRequest
::class ) {
353 } elseif ( $type === PasswordDomainAuthenticationRequest
::class ) {
354 $req = new $type( [] );
356 $req = $this->getMock( $type );
358 $req->action
= AuthManager
::ACTION_CHANGE
;
359 $req->username
= $user;
360 $req->password
= 'NewPassword';
361 $req->retype
= 'NewPassword';
363 $provider = $this->getProvider();
364 $this->validity
= $validity;
365 $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
366 $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
368 $req->retype
= 'BadRetype';
371 $provider->providerAllowsAuthenticationDataChange( $req, false )
374 $expect2->getValue() === 'ignored' ?
$expect2 : \StatusValue
::newFatal( 'badretype' ),
375 $provider->providerAllowsAuthenticationDataChange( $req, true )
378 $provider = $this->getProvider( true );
380 \StatusValue
::newGood( 'ignored' ),
381 $provider->providerAllowsAuthenticationDataChange( $req, true ),
382 'loginOnly mode should claim to ignore all changes'
386 public static function provideProviderAllowsAuthenticationDataChange() {
387 $err = \StatusValue
::newGood();
388 $err->error( 'arbitrary-warning' );
391 [ AuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
392 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
393 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
394 \StatusValue
::newGood(), \StatusValue
::newGood() ],
395 [ PasswordAuthenticationRequest
::class, 'uTSysop', \Status
::newGood(),
396 \StatusValue
::newGood(), \StatusValue
::newGood() ],
397 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::wrap( $err ),
398 \StatusValue
::newGood(), $err ],
399 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newFatal( 'arbitrary-error' ),
400 \StatusValue
::newGood(), \StatusValue
::newFatal( 'arbitrary-error' ) ],
401 [ PasswordAuthenticationRequest
::class, 'DoesNotExist', \Status
::newGood(),
402 \StatusValue
::newGood(), \StatusValue
::newGood( 'ignored' ) ],
403 [ PasswordDomainAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
404 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
409 * @dataProvider provideProviderChangeAuthenticationData
410 * @param string $user
411 * @param string $type
412 * @param bool $loginOnly
413 * @param bool $changed
415 public function testProviderChangeAuthenticationData( $user, $type, $loginOnly, $changed ) {
416 $cuser = ucfirst( $user );
417 $oldpass = 'UTSysopPassword';
418 $newpass = 'NewPassword';
420 $dbw = wfGetDB( DB_MASTER
);
421 $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => $cuser ] );
422 $oldExpiry = $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] );
423 $cb = new \
ScopedCallback( function () use ( $dbw, $cuser, $oldHash, $oldExpiry ) {
427 'user_password' => $oldHash,
428 'user_password_expires' => $oldExpiry,
430 [ 'user_name' => $cuser ]
434 $this->mergeMwGlobalArrayValue( 'wgHooks', [
435 'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
436 $expires = '30001231235959';
440 $provider = $this->getProvider( $loginOnly );
443 $loginReq = new PasswordAuthenticationRequest();
444 $loginReq->action
= AuthManager
::ACTION_LOGIN
;
445 $loginReq->username
= $user;
446 $loginReq->password
= $oldpass;
447 $loginReqs = [ PasswordAuthenticationRequest
::class => $loginReq ];
449 AuthenticationResponse
::newPass( $cuser ),
450 $provider->beginPrimaryAuthentication( $loginReqs ),
454 if ( $type === PasswordAuthenticationRequest
::class ) {
455 $changeReq = new $type();
457 $changeReq = $this->getMock( $type );
459 $changeReq->action
= AuthManager
::ACTION_CHANGE
;
460 $changeReq->username
= $user;
461 $changeReq->password
= $newpass;
462 $provider->providerChangeAuthenticationData( $changeReq );
467 $expectExpiry = null;
468 } elseif ( $changed ) {
471 $expectExpiry = '30001231235959';
475 $expectExpiry = $oldExpiry;
478 $loginReq->password
= $oldpass;
479 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
480 if ( $old === 'pass' ) {
482 AuthenticationResponse
::newPass( $cuser ),
484 'old password should pass'
488 AuthenticationResponse
::FAIL
,
490 'old password should fail'
494 $ret->message
->getKey(),
495 'old password should fail'
499 $loginReq->password
= $newpass;
500 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
501 if ( $new === 'pass' ) {
503 AuthenticationResponse
::newPass( $cuser ),
505 'new password should pass'
509 AuthenticationResponse
::FAIL
,
511 'new password should fail'
515 $ret->message
->getKey(),
516 'new password should fail'
522 $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] )
526 public static function provideProviderChangeAuthenticationData() {
528 [ 'UTSysop', AuthenticationRequest
::class, false, false ],
529 [ 'UTSysop', PasswordAuthenticationRequest
::class, false, true ],
530 [ 'UTSysop', AuthenticationRequest
::class, true, false ],
531 [ 'UTSysop', PasswordAuthenticationRequest
::class, true, true ],
532 [ 'uTSysop', PasswordAuthenticationRequest
::class, false, true ],
533 [ 'uTSysop', PasswordAuthenticationRequest
::class, true, true ],
537 public function testTestForAccountCreation() {
538 $user = \User
::newFromName( 'foo' );
539 $req = new PasswordAuthenticationRequest();
540 $req->action
= AuthManager
::ACTION_CREATE
;
541 $req->username
= 'Foo';
542 $req->password
= 'Bar';
543 $req->retype
= 'Bar';
544 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
546 $provider = $this->getProvider();
548 \StatusValue
::newGood(),
549 $provider->testForAccountCreation( $user, $user, [] ),
550 'No password request'
554 \StatusValue
::newGood(),
555 $provider->testForAccountCreation( $user, $user, $reqs ),
556 'Password request, validated'
559 $req->retype
= 'Baz';
561 \StatusValue
::newFatal( 'badretype' ),
562 $provider->testForAccountCreation( $user, $user, $reqs ),
563 'Password request, bad retype'
565 $req->retype
= 'Bar';
567 $this->validity
->error( 'arbitrary warning' );
568 $expect = \StatusValue
::newGood();
569 $expect->error( 'arbitrary warning' );
572 $provider->testForAccountCreation( $user, $user, $reqs ),
573 'Password request, not validated'
576 $provider = $this->getProvider( true );
577 $this->validity
->error( 'arbitrary warning' );
579 \StatusValue
::newGood(),
580 $provider->testForAccountCreation( $user, $user, $reqs ),
581 'Password request, not validated, loginOnly'
585 public function testAccountCreation() {
586 $user = \User
::newFromName( 'Foo' );
588 $req = new PasswordAuthenticationRequest();
589 $req->action
= AuthManager
::ACTION_CREATE
;
590 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
592 $provider = $this->getProvider( true );
594 $provider->beginPrimaryAccountCreation( $user, $user, [] );
595 $this->fail( 'Expected exception was not thrown' );
596 } catch ( \BadMethodCallException
$ex ) {
598 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
603 $provider->finishAccountCreation( $user, $user, AuthenticationResponse
::newPass() );
604 $this->fail( 'Expected exception was not thrown' );
605 } catch ( \BadMethodCallException
$ex ) {
607 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
611 $provider = $this->getProvider( false );
614 AuthenticationResponse
::newAbstain(),
615 $provider->beginPrimaryAccountCreation( $user, $user, [] )
618 $req->username
= 'foo';
619 $req->password
= null;
621 AuthenticationResponse
::newAbstain(),
622 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
625 $req->username
= null;
626 $req->password
= 'bar';
628 AuthenticationResponse
::newAbstain(),
629 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
632 $req->username
= 'foo';
633 $req->password
= 'bar';
635 $expect = AuthenticationResponse
::newPass( 'Foo' );
636 $expect->createRequest
= clone( $req );
637 $expect->createRequest
->username
= 'Foo';
638 $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
640 // We have to cheat a bit to avoid having to add a new user to
641 // the database to test the actual setting of the password works right
642 $dbw = wfGetDB( DB_MASTER
);
643 $oldHash = $dbw->selectField( 'user', 'user_password', [ 'user_name' => $user ] );
644 $cb = new \
ScopedCallback( function () use ( $dbw, $user, $oldHash ) {
645 $dbw->update( 'user', [ 'user_password' => $oldHash ], [ 'user_name' => $user ] );
648 $user = \User
::newFromName( 'UTSysop' );
649 $req->username
= $user->getName();
650 $req->password
= 'NewPassword';
651 $expect = AuthenticationResponse
::newPass( 'UTSysop' );
652 $expect->createRequest
= $req;
654 $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
655 $this->assertEquals( $expect, $res2, 'Sanity check' );
657 $ret = $provider->beginPrimaryAuthentication( $reqs );
658 $this->assertEquals( AuthenticationResponse
::FAIL
, $ret->status
, 'sanity check' );
660 $this->assertNull( $provider->finishAccountCreation( $user, $user, $res2 ) );
661 $ret = $provider->beginPrimaryAuthentication( $reqs );
662 $this->assertEquals( AuthenticationResponse
::PASS
, $ret->status
, 'new password is set' );