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 )
335 * @dataProvider provideProviderAllowsAuthenticationDataChange
336 * @param string $type
337 * @param string $user
338 * @param \Status $validity Result of the password validity check
339 * @param \StatusValue $expect1 Expected result with $checkData = false
340 * @param \StatusValue $expect2 Expected result with $checkData = true
342 public function testProviderAllowsAuthenticationDataChange( $type, $user, \Status
$validity,
343 \StatusValue
$expect1, \StatusValue
$expect2
345 if ( $type === PasswordAuthenticationRequest
::class ) {
347 } elseif ( $type === PasswordDomainAuthenticationRequest
::class ) {
348 $req = new $type( [] );
350 $req = $this->getMock( $type );
352 $req->action
= AuthManager
::ACTION_CHANGE
;
353 $req->username
= $user;
354 $req->password
= 'NewPassword';
355 $req->retype
= 'NewPassword';
357 $provider = $this->getProvider();
358 $this->validity
= $validity;
359 $this->assertEquals( $expect1, $provider->providerAllowsAuthenticationDataChange( $req, false ) );
360 $this->assertEquals( $expect2, $provider->providerAllowsAuthenticationDataChange( $req, true ) );
362 $req->retype
= 'BadRetype';
365 $provider->providerAllowsAuthenticationDataChange( $req, false )
368 $expect2->getValue() === 'ignored' ?
$expect2 : \StatusValue
::newFatal( 'badretype' ),
369 $provider->providerAllowsAuthenticationDataChange( $req, true )
372 $provider = $this->getProvider( true );
374 \StatusValue
::newGood( 'ignored' ),
375 $provider->providerAllowsAuthenticationDataChange( $req, true ),
376 'loginOnly mode should claim to ignore all changes'
380 public static function provideProviderAllowsAuthenticationDataChange() {
381 $err = \StatusValue
::newGood();
382 $err->error( 'arbitrary-warning' );
385 [ AuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
386 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
387 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
388 \StatusValue
::newGood(), \StatusValue
::newGood() ],
389 [ PasswordAuthenticationRequest
::class, 'uTSysop', \Status
::newGood(),
390 \StatusValue
::newGood(), \StatusValue
::newGood() ],
391 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::wrap( $err ),
392 \StatusValue
::newGood(), $err ],
393 [ PasswordAuthenticationRequest
::class, 'UTSysop', \Status
::newFatal( 'arbitrary-error' ),
394 \StatusValue
::newGood(), \StatusValue
::newFatal( 'arbitrary-error' ) ],
395 [ PasswordAuthenticationRequest
::class, 'DoesNotExist', \Status
::newGood(),
396 \StatusValue
::newGood(), \StatusValue
::newGood( 'ignored' ) ],
397 [ PasswordDomainAuthenticationRequest
::class, 'UTSysop', \Status
::newGood(),
398 \StatusValue
::newGood( 'ignored' ), \StatusValue
::newGood( 'ignored' ) ],
403 * @dataProvider provideProviderChangeAuthenticationData
404 * @param callable|bool $usernameTransform
405 * @param string $type
406 * @param bool $loginOnly
407 * @param bool $changed
409 public function testProviderChangeAuthenticationData(
410 $usernameTransform, $type, $loginOnly, $changed ) {
411 $testUser = $this->getMutableTestUser();
412 $user = $testUser->getUser()->getName();
413 if ( is_callable( $usernameTransform ) ) {
414 $user = call_user_func( $usernameTransform, $user );
416 $cuser = ucfirst( $user );
417 $oldpass = $testUser->getPassword();
418 $newpass = 'NewPassword';
420 $dbw = wfGetDB( DB_MASTER
);
421 $oldExpiry = $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] );
423 $this->mergeMwGlobalArrayValue( 'wgHooks', [
424 'ResetPasswordExpiration' => [ function ( $user, &$expires ) {
425 $expires = '30001231235959';
429 $provider = $this->getProvider( $loginOnly );
432 $loginReq = new PasswordAuthenticationRequest();
433 $loginReq->action
= AuthManager
::ACTION_LOGIN
;
434 $loginReq->username
= $user;
435 $loginReq->password
= $oldpass;
436 $loginReqs = [ PasswordAuthenticationRequest
::class => $loginReq ];
438 AuthenticationResponse
::newPass( $cuser ),
439 $provider->beginPrimaryAuthentication( $loginReqs ),
443 if ( $type === PasswordAuthenticationRequest
::class ) {
444 $changeReq = new $type();
446 $changeReq = $this->getMock( $type );
448 $changeReq->action
= AuthManager
::ACTION_CHANGE
;
449 $changeReq->username
= $user;
450 $changeReq->password
= $newpass;
451 $provider->providerChangeAuthenticationData( $changeReq );
456 $expectExpiry = null;
457 } elseif ( $changed ) {
460 $expectExpiry = '30001231235959';
464 $expectExpiry = $oldExpiry;
467 $loginReq->password
= $oldpass;
468 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
469 if ( $old === 'pass' ) {
471 AuthenticationResponse
::newPass( $cuser ),
473 'old password should pass'
477 AuthenticationResponse
::FAIL
,
479 'old password should fail'
483 $ret->message
->getKey(),
484 'old password should fail'
488 $loginReq->password
= $newpass;
489 $ret = $provider->beginPrimaryAuthentication( $loginReqs );
490 if ( $new === 'pass' ) {
492 AuthenticationResponse
::newPass( $cuser ),
494 'new password should pass'
498 AuthenticationResponse
::FAIL
,
500 'new password should fail'
504 $ret->message
->getKey(),
505 'new password should fail'
511 $dbw->selectField( 'user', 'user_password_expires', [ 'user_name' => $cuser ] )
515 public static function provideProviderChangeAuthenticationData() {
517 [ false, AuthenticationRequest
::class, false, false ],
518 [ false, PasswordAuthenticationRequest
::class, false, true ],
519 [ false, AuthenticationRequest
::class, true, false ],
520 [ false, PasswordAuthenticationRequest
::class, true, true ],
521 [ 'ucfirst', PasswordAuthenticationRequest
::class, false, true ],
522 [ 'ucfirst', PasswordAuthenticationRequest
::class, true, true ],
526 public function testTestForAccountCreation() {
527 $user = \User
::newFromName( 'foo' );
528 $req = new PasswordAuthenticationRequest();
529 $req->action
= AuthManager
::ACTION_CREATE
;
530 $req->username
= 'Foo';
531 $req->password
= 'Bar';
532 $req->retype
= 'Bar';
533 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
535 $provider = $this->getProvider();
537 \StatusValue
::newGood(),
538 $provider->testForAccountCreation( $user, $user, [] ),
539 'No password request'
543 \StatusValue
::newGood(),
544 $provider->testForAccountCreation( $user, $user, $reqs ),
545 'Password request, validated'
548 $req->retype
= 'Baz';
550 \StatusValue
::newFatal( 'badretype' ),
551 $provider->testForAccountCreation( $user, $user, $reqs ),
552 'Password request, bad retype'
554 $req->retype
= 'Bar';
556 $this->validity
->error( 'arbitrary warning' );
557 $expect = \StatusValue
::newGood();
558 $expect->error( 'arbitrary warning' );
561 $provider->testForAccountCreation( $user, $user, $reqs ),
562 'Password request, not validated'
565 $provider = $this->getProvider( true );
566 $this->validity
->error( 'arbitrary warning' );
568 \StatusValue
::newGood(),
569 $provider->testForAccountCreation( $user, $user, $reqs ),
570 'Password request, not validated, loginOnly'
574 public function testAccountCreation() {
575 $user = \User
::newFromName( 'Foo' );
577 $req = new PasswordAuthenticationRequest();
578 $req->action
= AuthManager
::ACTION_CREATE
;
579 $reqs = [ PasswordAuthenticationRequest
::class => $req ];
581 $provider = $this->getProvider( true );
583 $provider->beginPrimaryAccountCreation( $user, $user, [] );
584 $this->fail( 'Expected exception was not thrown' );
585 } catch ( \BadMethodCallException
$ex ) {
587 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
592 $provider->finishAccountCreation( $user, $user, AuthenticationResponse
::newPass() );
593 $this->fail( 'Expected exception was not thrown' );
594 } catch ( \BadMethodCallException
$ex ) {
596 'Shouldn\'t call this when accountCreationType() is NONE', $ex->getMessage()
600 $provider = $this->getProvider( false );
603 AuthenticationResponse
::newAbstain(),
604 $provider->beginPrimaryAccountCreation( $user, $user, [] )
607 $req->username
= 'foo';
608 $req->password
= null;
610 AuthenticationResponse
::newAbstain(),
611 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
614 $req->username
= null;
615 $req->password
= 'bar';
617 AuthenticationResponse
::newAbstain(),
618 $provider->beginPrimaryAccountCreation( $user, $user, $reqs )
621 $req->username
= 'foo';
622 $req->password
= 'bar';
624 $expect = AuthenticationResponse
::newPass( 'Foo' );
625 $expect->createRequest
= clone( $req );
626 $expect->createRequest
->username
= 'Foo';
627 $this->assertEquals( $expect, $provider->beginPrimaryAccountCreation( $user, $user, $reqs ) );
629 // We have to cheat a bit to avoid having to add a new user to
630 // the database to test the actual setting of the password works right
631 $dbw = wfGetDB( DB_MASTER
);
633 $user = \User
::newFromName( 'UTSysop' );
634 $req->username
= $user->getName();
635 $req->password
= 'NewPassword';
636 $expect = AuthenticationResponse
::newPass( 'UTSysop' );
637 $expect->createRequest
= $req;
639 $res2 = $provider->beginPrimaryAccountCreation( $user, $user, $reqs );
640 $this->assertEquals( $expect, $res2, 'Sanity check' );
642 $ret = $provider->beginPrimaryAuthentication( $reqs );
643 $this->assertEquals( AuthenticationResponse
::FAIL
, $ret->status
, 'sanity check' );
645 $this->assertNull( $provider->finishAccountCreation( $user, $user, $res2 ) );
646 $ret = $provider->beginPrimaryAuthentication( $reqs );
647 $this->assertEquals( AuthenticationResponse
::PASS
, $ret->status
, 'new password is set' );