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;
17 * Get an instance of the provider
19 * $provider->checkPasswordValidity is mocked to return $this->validity,
20 * because we don't need to test that here.
22 * @param bool $loginOnly
23 * @return LocalPasswordPrimaryAuthenticationProvider
25 protected function getProvider( $loginOnly = false ) {
26 if ( !$this->config
) {
27 $this->config
= new \
HashConfig();
29 $config = new \
MultiConfig( [
31 \ConfigFactory
::getDefaultInstance()->makeConfig( 'main' )
34 if ( !$this->manager
) {
35 $this->manager
= new AuthManager( new \
FauxRequest(), $config );
37 $this->validity
= \Status
::newGood();
39 $provider = $this->getMock(
40 LocalPasswordPrimaryAuthenticationProvider
::class,
41 [ 'checkPasswordValidity' ],
42 [ [ 'loginOnly' => $loginOnly ] ]
44 $provider->expects( $this->any() )->method( 'checkPasswordValidity' )
45 ->will( $this->returnCallback( function () {
46 return $this->validity
;
48 $provider->setConfig( $config );
49 $provider->setLogger( new \Psr\Log\
NullLogger() );
50 $provider->setManager( $this->manager
);
55 public function testBasics() {
56 $user = $this->getMutableTestUser()->getUser();
57 $userName = $user->getName();
58 $lowerInitialUserName = mb_strtolower( $userName[0] ) . substr( $userName, 1 );
60 $provider = new LocalPasswordPrimaryAuthenticationProvider();
63 PrimaryAuthenticationProvider
::TYPE_CREATE
,
64 $provider->accountCreationType()
67 $this->assertTrue( $provider->testUserExists( $userName ) );
68 $this->assertTrue( $provider->testUserExists( $lowerInitialUserName ) );
69 $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
70 $this->assertFalse( $provider->testUserExists( '<invalid>' ) );
72 $provider = new LocalPasswordPrimaryAuthenticationProvider( [ 'loginOnly' => true ] );
75 PrimaryAuthenticationProvider
::TYPE_NONE
,
76 $provider->accountCreationType()
79 $this->assertTrue( $provider->testUserExists( $userName ) );
80 $this->assertFalse( $provider->testUserExists( 'DoesNotExist' ) );
82 $req = new PasswordAuthenticationRequest
;
83 $req->action
= AuthManager
::ACTION_CHANGE
;
84 $req->username
= '<invalid>';
85 $provider->providerChangeAuthenticationData( $req );
88 public function testTestUserCanAuthenticate() {
89 $user = $this->getMutableTestUser()->getUser();
90 $userName = $user->getName();
91 $dbw = wfGetDB( DB_MASTER
);
93 $provider = $this->getProvider();
95 $this->assertFalse( $provider->testUserCanAuthenticate( '<invalid>' ) );
97 $this->assertFalse( $provider->testUserCanAuthenticate( 'DoesNotExist' ) );
99 $this->assertTrue( $provider->testUserCanAuthenticate( $userName ) );
100 $lowerInitialUserName = mb_strtolower( $userName[0] ) . substr( $userName, 1 );
101 $this->assertTrue( $provider->testUserCanAuthenticate( $lowerInitialUserName ) );
105 [ 'user_password' => \PasswordFactory
::newInvalidPassword()->toString() ],
106 [ 'user_name' => $userName ]
108 $this->assertFalse( $provider->testUserCanAuthenticate( $userName ) );
113 [ 'user_password' => '0123456789abcdef0123456789abcdef' ],
114 [ 'user_name' => $userName ]
116 $this->assertTrue( $provider->testUserCanAuthenticate( $userName ) );
119 public function testSetPasswordResetFlag() {
121 $this->getProvider();
123 /// @todo: Because we're currently using User, which uses the global config...
124 $this->setMwGlobals( [ 'wgPasswordExpireGrace' => 100 ] );
126 $this->config
->set( 'PasswordExpireGrace', 100 );
127 $this->config
->set( 'InvalidPasswordReset', true );
129 $provider = new LocalPasswordPrimaryAuthenticationProvider();
130 $provider->setConfig( $this->config
);
131 $provider->setLogger( new \Psr\Log\
NullLogger() );
132 $provider->setManager( $this->manager
);
133 $providerPriv = \TestingAccessWrapper
::newFromObject( $provider );
135 $user = $this->getMutableTestUser()->getUser();
136 $userName = $user->getName();
137 $dbw = wfGetDB( DB_MASTER
);
138 $row = $dbw->selectRow(
141 [ 'user_name' => $userName ],
145 $this->manager
->removeAuthenticationSessionData( null );
146 $row->user_password_expires
= wfTimestamp( TS_MW
, time() +
200 );
147 $providerPriv->setPasswordResetFlag( $userName, \Status
::newGood(), $row );
148 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
150 $this->manager
->removeAuthenticationSessionData( null );
151 $row->user_password_expires
= wfTimestamp( TS_MW
, time() - 200 );
152 $providerPriv->setPasswordResetFlag( $userName, \Status
::newGood(), $row );
153 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
154 $this->assertNotNull( $ret );
155 $this->assertSame( 'resetpass-expired', $ret->msg
->getKey() );
156 $this->assertTrue( $ret->hard
);
158 $this->manager
->removeAuthenticationSessionData( null );
159 $row->user_password_expires
= wfTimestamp( TS_MW
, time() - 1 );
160 $providerPriv->setPasswordResetFlag( $userName, \Status
::newGood(), $row );
161 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
162 $this->assertNotNull( $ret );
163 $this->assertSame( 'resetpass-expired-soft', $ret->msg
->getKey() );
164 $this->assertFalse( $ret->hard
);
166 $this->manager
->removeAuthenticationSessionData( null );
167 $row->user_password_expires
= null;
168 $status = \Status
::newGood();
169 $status->error( 'testing' );
170 $providerPriv->setPasswordResetFlag( $userName, $status, $row );
171 $ret = $this->manager
->getAuthenticationSessionData( 'reset-pass' );
172 $this->assertNotNull( $ret );
173 $this->assertSame( 'resetpass-validity-soft', $ret->msg
->getKey() );
174 $this->assertFalse( $ret->hard
);
177 public function testAuthentication() {
178 $testUser = $this->getMutableTestUser();
179 $userName = $testUser->getUser()->getName();
181 $dbw = wfGetDB( DB_MASTER
);
182 $id = \User
::idFromName( $userName );
184 $req = new PasswordAuthenticationRequest();
185 $req->action
= AuthManager
::ACTION_LOGIN
;
186 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
188 $provider = $this->getProvider();
192 AuthenticationResponse
::newAbstain(),
193 $provider->beginPrimaryAuthentication( [] )
196 $req->username
= 'foo';
197 $req->password
= null;
199 AuthenticationResponse
::newAbstain(),
200 $provider->beginPrimaryAuthentication( $reqs )
203 $req->username
= null;
204 $req->password
= 'bar';
206 AuthenticationResponse
::newAbstain(),
207 $provider->beginPrimaryAuthentication( $reqs )
210 $req->username
= '<invalid>';
211 $req->password
= 'WhoCares';
212 $ret = $provider->beginPrimaryAuthentication( $reqs );
214 AuthenticationResponse
::newAbstain(),
215 $provider->beginPrimaryAuthentication( $reqs )
218 $req->username
= 'DoesNotExist';
219 $req->password
= 'DoesNotExist';
220 $ret = $provider->beginPrimaryAuthentication( $reqs );
222 AuthenticationResponse
::newAbstain(),
223 $provider->beginPrimaryAuthentication( $reqs )
226 // Validation failure
227 $req->username
= $userName;
228 $req->password
= $testUser->getPassword();
229 $this->validity
= \Status
::newFatal( 'arbitrary-failure' );
230 $ret = $provider->beginPrimaryAuthentication( $reqs );
232 AuthenticationResponse
::FAIL
,
237 $ret->message
->getKey()
241 $this->manager
->removeAuthenticationSessionData( null );
242 $this->validity
= \Status
::newGood();
244 AuthenticationResponse
::newPass( $userName ),
245 $provider->beginPrimaryAuthentication( $reqs )
247 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
249 // Successful auth after normalizing name
250 $this->manager
->removeAuthenticationSessionData( null );
251 $this->validity
= \Status
::newGood();
252 $req->username
= mb_strtolower( $userName[0] ) . substr( $userName, 1 );
254 AuthenticationResponse
::newPass( $userName ),
255 $provider->beginPrimaryAuthentication( $reqs )
257 $this->assertNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
258 $req->username
= $userName;
260 // Successful auth with reset
261 $this->manager
->removeAuthenticationSessionData( null );
262 $this->validity
->error( 'arbitrary-warning' );
264 AuthenticationResponse
::newPass( $userName ),
265 $provider->beginPrimaryAuthentication( $reqs )
267 $this->assertNotNull( $this->manager
->getAuthenticationSessionData( 'reset-pass' ) );
270 $this->validity
= \Status
::newGood();
271 $req->password
= 'Wrong';
272 $ret = $provider->beginPrimaryAuthentication( $reqs );
274 AuthenticationResponse
::FAIL
,
279 $ret->message
->getKey()
282 // Correct handling of legacy encodings
283 $password = ':B:salt:' . md5( 'salt-' . md5( "\xe1\xe9\xed\xf3\xfa" ) );
284 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
285 $req->password
= 'áéÃóú';
286 $ret = $provider->beginPrimaryAuthentication( $reqs );
288 AuthenticationResponse
::FAIL
,
293 $ret->message
->getKey()
296 $this->config
->set( 'LegacyEncoding', true );
298 AuthenticationResponse
::newPass( $userName ),
299 $provider->beginPrimaryAuthentication( $reqs )
302 $req->password
= 'áéÃóú Wrong';
303 $ret = $provider->beginPrimaryAuthentication( $reqs );
305 AuthenticationResponse
::FAIL
,
310 $ret->message
->getKey()
313 // Correct handling of really old password hashes
314 $this->config
->set( 'PasswordSalt', false );
315 $password = md5( 'FooBar' );
316 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
317 $req->password
= 'FooBar';
319 AuthenticationResponse
::newPass( $userName ),
320 $provider->beginPrimaryAuthentication( $reqs )
323 $this->config
->set( 'PasswordSalt', true );
324 $password = md5( "$id-" . md5( 'FooBar' ) );
325 $dbw->update( 'user', [ 'user_password' => $password ], [ 'user_name' => $userName ] );
326 $req->password
= 'FooBar';
328 AuthenticationResponse
::newPass( $userName ),
329 $provider->beginPrimaryAuthentication( $reqs )
334 * @dataProvider provideProviderAllowsAuthenticationDataChange
335 * @param string $type
336 * @param string $user
337 * @param \Status $validity Result of the password validity check
338 * @param \StatusValue $expect1 Expected result with $checkData = false
339 * @param \StatusValue $expect2 Expected result with $checkData = true
341 public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status
$validity,
342 \StatusValue
$expect1, \StatusValue
$expect2
344 if ( $type === PasswordAuthenticationRequest
::class ) {
346 } elseif ( $type === PasswordDomainAuthenticationRequest
::class ) {
347 $req = new $type( [] );
349 $req = $this->getMock( $type );
351 $req->action
= AuthManager
::ACTION_CHANGE
;
352 $req->username
= $user;
353 $req->password
= 'NewPassword';
354 $req->retype
= 'NewPassword';
356 $provider = $this->getProvider();
357 $this->validity
= $validity;
358 $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
359 $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
361 $req->retype
= 'BadRetype';
364 $provider->providerAllowsAuthenticationDataChange( $req, false )
367 $expect2->getValue() === 'ignored' ?
$expect2 : \StatusValue
::newFatal( 'badretype' ),
368 $provider->providerAllowsAuthenticationDataChange( $req, true )
371 $provider = $this->getProvider( true );
373 \StatusValue
::newGood( 'ignored' ),
374 $provider->providerAllowsAuthenticationDataChange( $req, true ),
375 'loginOnly mode should claim to ignore all changes'
379 public static function provideProviderAllowsAuthenticationDataChange() {
380 $err = \StatusValue
::newGood();
381 $err->error( 'arbitrary-warning' );
384 [ AuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
385 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
386 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
387 \StatusValue
::newGood(), \StatusValue
::newGood() ],
388 [ PasswordAuthenticationRequest
::class, 'uTSysop', \Status
::newGood(),
389 \StatusValue
::newGood(), \StatusValue
::newGood() ],
390 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::wrap( $err ),
391 \StatusValue
::newGood(), $err ],
392 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newFatal( 'arbitrary-error' ),
393 \StatusValue
::newGood(), \StatusValue
::newFatal( 'arbitrary-error' ) ],
394 [ PasswordAuthenticationRequest
::class, 'DoesNotExist', \Status
::newGood(),
395 \StatusValue
::newGood(), \StatusValue
::newGood( 'ignored' ) ],
396 [ PasswordDomainAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
397 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
402 * @dataProvider provideProviderChangeAuthenticationData
403 * @param callable|bool $usernameTransform
404 * @param string $type
405 * @param bool $loginOnly
406 * @param bool $changed
408 public function testProviderChangeAuthenticationData(
409 $usernameTransform, $type, $loginOnly, $changed ) {
410 $testUser = $this->getMutableTestUser();
411 $user = $testUser->getUser()->getName();
412 if ( is_callable( $usernameTransform ) ) {
413 $user = call_user_func( $usernameTransform, $user );
415 $cuser = ucfirst( $user );
416 $oldpass = $testUser->getPassword();
417 $newpass = 'NewPassword';
419 $dbw = wfGetDB( DB_MASTER
);
420 $oldExpiry = $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] );
422 $this->mergeMwGlobalArrayValue( 'wgHooks', [
423 'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
424 $expires = '30001231235959';
428 $provider = $this->getProvider( $loginOnly );
431 $loginReq = new PasswordAuthenticationRequest();
432 $loginReq->action
= AuthManager
::ACTION_LOGIN
;
433 $loginReq->username
= $user;
434 $loginReq->password
= $oldpass;
435 $loginReqs = [ PasswordAuthenticationRequest
::class => $loginReq ];
437 AuthenticationResponse
::newPass( $cuser ),
438 $provider->beginPrimaryAuthentication( $loginReqs ),
442 if ( $type === PasswordAuthenticationRequest
::class ) {
443 $changeReq = new $type();
445 $changeReq = $this->getMock( $type );
447 $changeReq->action
= AuthManager
::ACTION_CHANGE
;
448 $changeReq->username
= $user;
449 $changeReq->password
= $newpass;
450 $provider->providerChangeAuthenticationData( $changeReq );
455 $expectExpiry = null;
456 } elseif ( $changed ) {
459 $expectExpiry = '30001231235959';
463 $expectExpiry = $oldExpiry;
466 $loginReq->password
= $oldpass;
467 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
468 if ( $old === 'pass' ) {
470 AuthenticationResponse
::newPass( $cuser ),
472 'old password should pass'
476 AuthenticationResponse
::FAIL
,
478 'old password should fail'
482 $ret->message
->getKey(),
483 'old password should fail'
487 $loginReq->password
= $newpass;
488 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
489 if ( $new === 'pass' ) {
491 AuthenticationResponse
::newPass( $cuser ),
493 'new password should pass'
497 AuthenticationResponse
::FAIL
,
499 'new password should fail'
503 $ret->message
->getKey(),
504 'new password should fail'
510 $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] )
514 public static function provideProviderChangeAuthenticationData() {
516 [ false, AuthenticationRequest
::class, false, false ],
517 [ false, PasswordAuthenticationRequest
::class, false, true ],
518 [ false, AuthenticationRequest
::class, true, false ],
519 [ false, PasswordAuthenticationRequest
::class, true, true ],
520 [ 'ucfirst', PasswordAuthenticationRequest
::class, false, true ],
521 [ 'ucfirst', PasswordAuthenticationRequest
::class, true, true ],
525 public function testTestForAccountCreation() {
526 $user = \User
::newFromName( 'foo' );
527 $req = new PasswordAuthenticationRequest();
528 $req->action
= AuthManager
::ACTION_CREATE
;
529 $req->username
= 'Foo';
530 $req->password
= 'Bar';
531 $req->retype
= 'Bar';
532 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
534 $provider = $this->getProvider();
536 \StatusValue
::newGood(),
537 $provider->testForAccountCreation( $user, $user, [] ),
538 'No password request'
542 \StatusValue
::newGood(),
543 $provider->testForAccountCreation( $user, $user, $reqs ),
544 'Password request, validated'
547 $req->retype
= 'Baz';
549 \StatusValue
::newFatal( 'badretype' ),
550 $provider->testForAccountCreation( $user, $user, $reqs ),
551 'Password request, bad retype'
553 $req->retype
= 'Bar';
555 $this->validity
->error( 'arbitrary warning' );
556 $expect = \StatusValue
::newGood();
557 $expect->error( 'arbitrary warning' );
560 $provider->testForAccountCreation( $user, $user, $reqs ),
561 'Password request, not validated'
564 $provider = $this->getProvider( true );
565 $this->validity
->error( 'arbitrary warning' );
567 \StatusValue
::newGood(),
568 $provider->testForAccountCreation( $user, $user, $reqs ),
569 'Password request, not validated, loginOnly'
573 public function testAccountCreation() {
574 $user = \User
::newFromName( 'Foo' );
576 $req = new PasswordAuthenticationRequest();
577 $req->action
= AuthManager
::ACTION_CREATE
;
578 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
580 $provider = $this->getProvider( true );
582 $provider->beginPrimaryAccountCreation( $user, $user, [] );
583 $this->fail( 'Expected exception was not thrown' );
584 } catch ( \BadMethodCallException
$ex ) {
586 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
591 $provider->finishAccountCreation( $user, $user, AuthenticationResponse
::newPass() );
592 $this->fail( 'Expected exception was not thrown' );
593 } catch ( \BadMethodCallException
$ex ) {
595 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
599 $provider = $this->getProvider( false );
602 AuthenticationResponse
::newAbstain(),
603 $provider->beginPrimaryAccountCreation( $user, $user, [] )
606 $req->username
= 'foo';
607 $req->password
= null;
609 AuthenticationResponse
::newAbstain(),
610 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
613 $req->username
= null;
614 $req->password
= 'bar';
616 AuthenticationResponse
::newAbstain(),
617 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
620 $req->username
= 'foo';
621 $req->password
= 'bar';
623 $expect = AuthenticationResponse
::newPass( 'Foo' );
624 $expect->createRequest
= clone( $req );
625 $expect->createRequest
->username
= 'Foo';
626 $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
628 // We have to cheat a bit to avoid having to add a new user to
629 // the database to test the actual setting of the password works right
630 $dbw = wfGetDB( DB_MASTER
);
632 $user = \User
::newFromName( 'UTSysop' );
633 $req->username
= $user->getName();
634 $req->password
= 'NewPassword';
635 $expect = AuthenticationResponse
::newPass( 'UTSysop' );
636 $expect->createRequest
= $req;
638 $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
639 $this->assertEquals( $expect, $res2, 'Sanity check' );
641 $ret = $provider->beginPrimaryAuthentication( $reqs );
642 $this->assertEquals( AuthenticationResponse
::FAIL
, $ret->status
, 'sanity check' );
644 $this->assertNull( $provider->finishAccountCreation( $user, $user, $res2 ) );
645 $ret = $provider->beginPrimaryAuthentication( $reqs );
646 $this->assertEquals( AuthenticationResponse
::PASS
, $ret->status
, 'new password is set' );