3 * Authentication (and possibly Authorization in the future) system entry point
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 namespace MediaWiki\Auth
;
27 use Psr\Log\LoggerAwareInterface
;
28 use Psr\Log\LoggerInterface
;
35 * This serves as the entry point to the authentication system.
37 * In the future, it may also serve as the entry point to the authorization
43 class AuthManager
implements LoggerAwareInterface
{
44 /** Log in with an existing (not necessarily local) user */
45 const ACTION_LOGIN
= 'login';
46 /** Continue a login process that was interrupted by the need for user input or communication
47 * with an external provider */
48 const ACTION_LOGIN_CONTINUE
= 'login-continue';
49 /** Create a new user */
50 const ACTION_CREATE
= 'create';
51 /** Continue a user creation process that was interrupted by the need for user input or
52 * communication with an external provider */
53 const ACTION_CREATE_CONTINUE
= 'create-continue';
54 /** Link an existing user to a third-party account */
55 const ACTION_LINK
= 'link';
56 /** Continue a user linking process that was interrupted by the need for user input or
57 * communication with an external provider */
58 const ACTION_LINK_CONTINUE
= 'link-continue';
59 /** Change a user's credentials */
60 const ACTION_CHANGE
= 'change';
61 /** Remove a user's credentials */
62 const ACTION_REMOVE
= 'remove';
63 /** Like ACTION_REMOVE but for linking providers only */
64 const ACTION_UNLINK
= 'unlink';
66 /** Security-sensitive operations are ok. */
68 /** Security-sensitive operations should re-authenticate. */
69 const SEC_REAUTH
= 'reauth';
70 /** Security-sensitive should not be performed. */
71 const SEC_FAIL
= 'fail';
73 /** Auto-creation is due to SessionManager */
74 const AUTOCREATE_SOURCE_SESSION
= \MediaWiki\Session\SessionManager
::class;
76 /** @var AuthManager|null */
77 private static $instance = null;
79 /** @var WebRequest */
85 /** @var LoggerInterface */
88 /** @var AuthenticationProvider[] */
89 private $allAuthenticationProviders = [];
91 /** @var PreAuthenticationProvider[] */
92 private $preAuthenticationProviders = null;
94 /** @var PrimaryAuthenticationProvider[] */
95 private $primaryAuthenticationProviders = null;
97 /** @var SecondaryAuthenticationProvider[] */
98 private $secondaryAuthenticationProviders = null;
100 /** @var CreatedAccountAuthenticationRequest[] */
101 private $createdAccountAuthenticationRequests = [];
104 * Get the global AuthManager
105 * @return AuthManager
107 public static function singleton() {
108 global $wgDisableAuthManager;
110 if ( $wgDisableAuthManager ) {
111 throw new \
BadMethodCallException( '$wgDisableAuthManager is set' );
114 if ( self
::$instance === null ) {
115 self
::$instance = new self(
116 \RequestContext
::getMain()->getRequest(),
117 \ConfigFactory
::getDefaultInstance()->makeConfig( 'main' )
120 return self
::$instance;
124 * @param WebRequest $request
125 * @param Config $config
127 public function __construct( WebRequest
$request, Config
$config ) {
128 $this->request
= $request;
129 $this->config
= $config;
130 $this->setLogger( \MediaWiki\Logger\LoggerFactory
::getInstance( 'authentication' ) );
134 * @param LoggerInterface $logger
136 public function setLogger( LoggerInterface
$logger ) {
137 $this->logger
= $logger;
143 public function getRequest() {
144 return $this->request
;
148 * Force certain PrimaryAuthenticationProviders
149 * @deprecated For backwards compatibility only
150 * @param PrimaryAuthenticationProvider[] $providers
153 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
154 $this->logger
->warning( "Overriding AuthManager primary authn because $why" );
156 if ( $this->primaryAuthenticationProviders
!== null ) {
157 $this->logger
->warning(
158 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
161 $this->allAuthenticationProviders
= array_diff_key(
162 $this->allAuthenticationProviders
,
163 $this->primaryAuthenticationProviders
165 $session = $this->request
->getSession();
166 $session->remove( 'AuthManager::authnState' );
167 $session->remove( 'AuthManager::accountCreationState' );
168 $session->remove( 'AuthManager::accountLinkState' );
169 $this->createdAccountAuthenticationRequests
= [];
172 $this->primaryAuthenticationProviders
= [];
173 foreach ( $providers as $provider ) {
174 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
175 throw new \
RuntimeException(
176 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
177 get_class( $provider )
180 $provider->setLogger( $this->logger
);
181 $provider->setManager( $this );
182 $provider->setConfig( $this->config
);
183 $id = $provider->getUniqueId();
184 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
185 throw new \
RuntimeException(
186 "Duplicate specifications for id $id (classes " .
187 get_class( $provider ) . ' and ' .
188 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
191 $this->allAuthenticationProviders
[$id] = $provider;
192 $this->primaryAuthenticationProviders
[$id] = $provider;
197 * Call a legacy AuthPlugin method, if necessary
198 * @codeCoverageIgnore
199 * @deprecated For backwards compatibility only, should be avoided in new code
200 * @param string $method AuthPlugin method to call
201 * @param array $params Parameters to pass
202 * @param mixed $return Return value if AuthPlugin wasn't called
203 * @return mixed Return value from the AuthPlugin method, or $return
205 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
208 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin
) {
209 return call_user_func_array( [ $wgAuth, $method ], $params );
216 * @name Authentication
221 * Indicate whether user authentication is possible
223 * It may not be if the session is provided by something like OAuth
224 * for which each individual request includes authentication data.
228 public function canAuthenticateNow() {
229 return $this->request
->getSession()->canSetUser();
233 * Start an authentication flow
235 * In addition to the AuthenticationRequests returned by
236 * $this->getAuthenticationRequests(), a client might include a
237 * CreateFromLoginAuthenticationRequest from a previous login attempt to
240 * Instead of the AuthenticationRequests returned by
241 * $this->getAuthenticationRequests(), a client might pass a
242 * CreatedAccountAuthenticationRequest from an account creation that just
243 * succeeded to log in to the just-created account.
245 * @param AuthenticationRequest[] $reqs
246 * @param string $returnToUrl Url that REDIRECT responses should eventually
248 * @return AuthenticationResponse See self::continueAuthentication()
250 public function beginAuthentication( array $reqs, $returnToUrl ) {
251 $session = $this->request
->getSession();
252 if ( !$session->canSetUser() ) {
253 // Caller should have called canAuthenticateNow()
254 $session->remove( 'AuthManager::authnState' );
255 throw new \
LogicException( 'Authentication is not possible now' );
258 $guessUserName = null;
259 foreach ( $reqs as $req ) {
260 $req->returnToUrl
= $returnToUrl;
261 // @codeCoverageIgnoreStart
262 if ( $req->username
!== null && $req->username
!== '' ) {
263 if ( $guessUserName === null ) {
264 $guessUserName = $req->username
;
265 } elseif ( $guessUserName !== $req->username
) {
266 $guessUserName = null;
270 // @codeCoverageIgnoreEnd
273 // Check for special-case login of a just-created account
274 $req = AuthenticationRequest
::getRequestByClass(
275 $reqs, CreatedAccountAuthenticationRequest
::class
278 if ( !in_array( $req, $this->createdAccountAuthenticationRequests
, true ) ) {
279 throw new \
LogicException(
280 'CreatedAccountAuthenticationRequests are only valid on ' .
281 'the same AuthManager that created the account'
285 $user = User
::newFromName( $req->username
);
286 // @codeCoverageIgnoreStart
288 throw new \
UnexpectedValueException(
289 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
291 } elseif ( $user->getId() != $req->id
) {
292 throw new \
UnexpectedValueException(
293 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
296 // @codeCoverageIgnoreEnd
298 $this->logger
->info( 'Logging in {user} after account creation', [
299 'user' => $user->getName(),
301 $ret = AuthenticationResponse
::newPass( $user->getName() );
302 $this->setSessionDataForUser( $user );
303 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
304 $session->remove( 'AuthManager::authnState' );
305 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
309 $this->removeAuthenticationSessionData( null );
311 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
312 $status = $provider->testForAuthentication( $reqs );
313 if ( !$status->isGood() ) {
314 $this->logger
->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
315 $ret = AuthenticationResponse
::newFail(
316 Status
::wrap( $status )->getMessage()
318 $this->callMethodOnProviders( 7, 'postAuthentication',
319 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
321 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
328 'returnToUrl' => $returnToUrl,
329 'guessUserName' => $guessUserName,
331 'primaryResponse' => null,
334 'continueRequests' => [],
337 // Preserve state from a previous failed login
338 $req = AuthenticationRequest
::getRequestByClass(
339 $reqs, CreateFromLoginAuthenticationRequest
::class
342 $state['maybeLink'] = $req->maybeLink
;
345 $session = $this->request
->getSession();
346 $session->setSecret( 'AuthManager::authnState', $state );
349 return $this->continueAuthentication( $reqs );
353 * Continue an authentication flow
355 * Return values are interpreted as follows:
356 * - status FAIL: Authentication failed. If $response->createRequest is
357 * set, that may be passed to self::beginAuthentication() or to
358 * self::beginAccountCreation() to preserve state.
359 * - status REDIRECT: The client should be redirected to the contained URL,
360 * new AuthenticationRequests should be made (if any), then
361 * AuthManager::continueAuthentication() should be called.
362 * - status UI: The client should be presented with a user interface for
363 * the fields in the specified AuthenticationRequests, then new
364 * AuthenticationRequests should be made, then
365 * AuthManager::continueAuthentication() should be called.
366 * - status RESTART: The user logged in successfully with a third-party
367 * service, but the third-party credentials aren't attached to any local
368 * account. This could be treated as a UI or a FAIL.
369 * - status PASS: Authentication was successful.
371 * @param AuthenticationRequest[] $reqs
372 * @return AuthenticationResponse
374 public function continueAuthentication( array $reqs ) {
375 $session = $this->request
->getSession();
377 if ( !$session->canSetUser() ) {
378 // Caller should have called canAuthenticateNow()
379 // @codeCoverageIgnoreStart
380 throw new \
LogicException( 'Authentication is not possible now' );
381 // @codeCoverageIgnoreEnd
384 $state = $session->getSecret( 'AuthManager::authnState' );
385 if ( !is_array( $state ) ) {
386 return AuthenticationResponse
::newFail(
387 wfMessage( 'authmanager-authn-not-in-progress' )
390 $state['continueRequests'] = [];
392 $guessUserName = $state['guessUserName'];
394 foreach ( $reqs as $req ) {
395 $req->returnToUrl
= $state['returnToUrl'];
398 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
400 if ( $state['primary'] === null ) {
401 // We haven't picked a PrimaryAuthenticationProvider yet
402 // @codeCoverageIgnoreStart
403 $guessUserName = null;
404 foreach ( $reqs as $req ) {
405 if ( $req->username
!== null && $req->username
!== '' ) {
406 if ( $guessUserName === null ) {
407 $guessUserName = $req->username
;
408 } elseif ( $guessUserName !== $req->username
) {
409 $guessUserName = null;
414 $state['guessUserName'] = $guessUserName;
415 // @codeCoverageIgnoreEnd
416 $state['reqs'] = $reqs;
418 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
419 $res = $provider->beginPrimaryAuthentication( $reqs );
420 switch ( $res->status
) {
421 case AuthenticationResponse
::PASS
;
422 $state['primary'] = $id;
423 $state['primaryResponse'] = $res;
424 $this->logger
->debug( "Primary login with $id succeeded" );
426 case AuthenticationResponse
::FAIL
;
427 $this->logger
->debug( "Login failed in primary authentication by $id" );
428 if ( $res->createRequest ||
$state['maybeLink'] ) {
429 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
430 $res->createRequest
, $state['maybeLink']
433 $this->callMethodOnProviders( 7, 'postAuthentication',
434 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
436 $session->remove( 'AuthManager::authnState' );
437 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
439 case AuthenticationResponse
::ABSTAIN
;
442 case AuthenticationResponse
::REDIRECT
;
443 case AuthenticationResponse
::UI
;
444 $this->logger
->debug( "Primary login with $id returned $res->status" );
445 $state['primary'] = $id;
446 $state['continueRequests'] = $res->neededRequests
;
447 $session->setSecret( 'AuthManager::authnState', $state );
450 // @codeCoverageIgnoreStart
452 throw new \
DomainException(
453 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
455 // @codeCoverageIgnoreEnd
458 if ( $state['primary'] === null ) {
459 $this->logger
->debug( 'Login failed in primary authentication because no provider accepted' );
460 $ret = AuthenticationResponse
::newFail(
461 wfMessage( 'authmanager-authn-no-primary' )
463 $this->callMethodOnProviders( 7, 'postAuthentication',
464 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
466 $session->remove( 'AuthManager::authnState' );
469 } elseif ( $state['primaryResponse'] === null ) {
470 $provider = $this->getAuthenticationProvider( $state['primary'] );
471 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
472 // Configuration changed? Force them to start over.
473 // @codeCoverageIgnoreStart
474 $ret = AuthenticationResponse
::newFail(
475 wfMessage( 'authmanager-authn-not-in-progress' )
477 $this->callMethodOnProviders( 7, 'postAuthentication',
478 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
480 $session->remove( 'AuthManager::authnState' );
482 // @codeCoverageIgnoreEnd
484 $id = $provider->getUniqueId();
485 $res = $provider->continuePrimaryAuthentication( $reqs );
486 switch ( $res->status
) {
487 case AuthenticationResponse
::PASS
;
488 $state['primaryResponse'] = $res;
489 $this->logger
->debug( "Primary login with $id succeeded" );
491 case AuthenticationResponse
::FAIL
;
492 $this->logger
->debug( "Login failed in primary authentication by $id" );
493 if ( $res->createRequest ||
$state['maybeLink'] ) {
494 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
495 $res->createRequest
, $state['maybeLink']
498 $this->callMethodOnProviders( 7, 'postAuthentication',
499 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
501 $session->remove( 'AuthManager::authnState' );
502 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
504 case AuthenticationResponse
::REDIRECT
;
505 case AuthenticationResponse
::UI
;
506 $this->logger
->debug( "Primary login with $id returned $res->status" );
507 $state['continueRequests'] = $res->neededRequests
;
508 $session->setSecret( 'AuthManager::authnState', $state );
511 throw new \
DomainException(
512 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
517 $res = $state['primaryResponse'];
518 if ( $res->username
=== null ) {
519 $provider = $this->getAuthenticationProvider( $state['primary'] );
520 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
521 // Configuration changed? Force them to start over.
522 // @codeCoverageIgnoreStart
523 $ret = AuthenticationResponse
::newFail(
524 wfMessage( 'authmanager-authn-not-in-progress' )
526 $this->callMethodOnProviders( 7, 'postAuthentication',
527 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
529 $session->remove( 'AuthManager::authnState' );
531 // @codeCoverageIgnoreEnd
534 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
&&
536 // don't confuse the user with an incorrect message if linking is disabled
537 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider
::class )
539 $state['maybeLink'][$res->linkRequest
->getUniqueId()] = $res->linkRequest
;
540 $msg = 'authmanager-authn-no-local-user-link';
542 $msg = 'authmanager-authn-no-local-user';
544 $this->logger
->debug(
545 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
547 $ret = AuthenticationResponse
::newRestart( wfMessage( $msg ) );
548 $ret->neededRequests
= $this->getAuthenticationRequestsInternal(
551 $this->getPrimaryAuthenticationProviders() +
$this->getSecondaryAuthenticationProviders()
553 if ( $res->createRequest ||
$state['maybeLink'] ) {
554 $ret->createRequest
= new CreateFromLoginAuthenticationRequest(
555 $res->createRequest
, $state['maybeLink']
557 $ret->neededRequests
[] = $ret->createRequest
;
559 $session->setSecret( 'AuthManager::authnState', [
560 'reqs' => [], // Will be filled in later
562 'primaryResponse' => null,
564 'continueRequests' => $ret->neededRequests
,
569 // Step 2: Primary authentication succeeded, create the User object
570 // (and add the user locally if necessary)
572 $user = User
::newFromName( $res->username
, 'usable' );
574 throw new \
DomainException(
575 get_class( $provider ) . " returned an invalid username: {$res->username}"
578 if ( $user->getId() === 0 ) {
579 // User doesn't exist locally. Create it.
580 $this->logger
->info( 'Auto-creating {user} on login', [
581 'user' => $user->getName(),
583 $status = $this->autoCreateUser( $user, $state['primary'], false );
584 if ( !$status->isGood() ) {
585 $ret = AuthenticationResponse
::newFail(
586 Status
::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
588 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
589 $session->remove( 'AuthManager::authnState' );
590 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
595 // Step 3: Iterate over all the secondary authentication providers.
597 $beginReqs = $state['reqs'];
599 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
600 if ( !isset( $state['secondary'][$id] ) ) {
601 // This provider isn't started yet, so we pass it the set
602 // of reqs from beginAuthentication instead of whatever
603 // might have been used by a previous provider in line.
604 $func = 'beginSecondaryAuthentication';
605 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
606 } elseif ( !$state['secondary'][$id] ) {
607 $func = 'continueSecondaryAuthentication';
608 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
612 switch ( $res->status
) {
613 case AuthenticationResponse
::PASS
;
614 $this->logger
->debug( "Secondary login with $id succeeded" );
616 case AuthenticationResponse
::ABSTAIN
;
617 $state['secondary'][$id] = true;
619 case AuthenticationResponse
::FAIL
;
620 $this->logger
->debug( "Login failed in secondary authentication by $id" );
621 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
622 $session->remove( 'AuthManager::authnState' );
623 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
625 case AuthenticationResponse
::REDIRECT
;
626 case AuthenticationResponse
::UI
;
627 $this->logger
->debug( "Secondary login with $id returned " . $res->status
);
628 $state['secondary'][$id] = false;
629 $state['continueRequests'] = $res->neededRequests
;
630 $session->setSecret( 'AuthManager::authnState', $state );
633 // @codeCoverageIgnoreStart
635 throw new \
DomainException(
636 get_class( $provider ) . "::{$func}() returned $res->status"
638 // @codeCoverageIgnoreEnd
642 // Step 4: Authentication complete! Set the user in the session and
645 $this->logger
->info( 'Login for {user} succeeded', [
646 'user' => $user->getName(),
648 $req = AuthenticationRequest
::getRequestByClass(
649 $beginReqs, RememberMeAuthenticationRequest
::class
651 $this->setSessionDataForUser( $user, $req && $req->rememberMe
);
652 $ret = AuthenticationResponse
::newPass( $user->getName() );
653 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
654 $session->remove( 'AuthManager::authnState' );
655 $this->removeAuthenticationSessionData( null );
656 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
658 } catch ( \Exception
$ex ) {
659 $session->remove( 'AuthManager::authnState' );
665 * Whether security-sensitive operations should proceed.
667 * A "security-sensitive operation" is something like a password or email
668 * change, that would normally have a "reenter your password to confirm"
669 * box if we only supported password-based authentication.
671 * @param string $operation Operation being checked. This should be a
672 * message-key-like string such as 'change-password' or 'change-email'.
673 * @return string One of the SEC_* constants.
675 public function securitySensitiveOperationStatus( $operation ) {
676 $status = self
::SEC_OK
;
678 $this->logger
->debug( __METHOD__
. ": Checking $operation" );
680 $session = $this->request
->getSession();
681 $aId = $session->getUser()->getId();
683 // User isn't authenticated. DWIM?
684 $status = $this->canAuthenticateNow() ? self
::SEC_REAUTH
: self
::SEC_FAIL
;
685 $this->logger
->info( __METHOD__
. ": Not logged in! $operation is $status" );
689 if ( $session->canSetUser() ) {
690 $id = $session->get( 'AuthManager:lastAuthId' );
691 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
692 if ( $id !== $aId ||
$last === null ) {
693 $timeSinceLogin = PHP_INT_MAX
; // Forever ago
695 $timeSinceLogin = max( 0, time() - $last );
698 $thresholds = $this->config
->get( 'ReauthenticateTime' );
699 if ( isset( $thresholds[$operation] ) ) {
700 $threshold = $thresholds[$operation];
701 } elseif ( isset( $thresholds['default'] ) ) {
702 $threshold = $thresholds['default'];
704 throw new \
UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
707 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
708 $status = self
::SEC_REAUTH
;
711 $timeSinceLogin = -1;
713 $pass = $this->config
->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
714 if ( isset( $pass[$operation] ) ) {
715 $status = $pass[$operation] ? self
::SEC_OK
: self
::SEC_FAIL
;
716 } elseif ( isset( $pass['default'] ) ) {
717 $status = $pass['default'] ? self
::SEC_OK
: self
::SEC_FAIL
;
719 throw new \
UnexpectedValueException(
720 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
725 \Hooks
::run( 'SecuritySensitiveOperationStatus', [
726 &$status, $operation, $session, $timeSinceLogin
729 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
730 if ( !$this->canAuthenticateNow() && $status === self
::SEC_REAUTH
) {
731 $status = self
::SEC_FAIL
;
734 $this->logger
->info( __METHOD__
. ": $operation is $status" );
740 * Determine whether a username can authenticate
742 * @param string $username
745 public function userCanAuthenticate( $username ) {
746 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
747 if ( $provider->testUserCanAuthenticate( $username ) ) {
755 * Provide normalized versions of the username for security checks
757 * Since different providers can normalize the input in different ways,
758 * this returns an array of all the different ways the name might be
759 * normalized for authentication.
761 * The returned strings should not be revealed to the user, as that might
762 * leak private information (e.g. an email address might be normalized to a
765 * @param string $username
768 public function normalizeUsername( $username ) {
770 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
771 $normalized = $provider->providerNormalizeUsername( $username );
772 if ( $normalized !== null ) {
773 $ret[$normalized] = true;
776 return array_keys( $ret );
782 * @name Authentication data changing
787 * Revoke any authentication credentials for a user
789 * After this, the user should no longer be able to log in.
791 * @param string $username
793 public function revokeAccessForUser( $username ) {
794 $this->logger
->info( 'Revoking access for {user}', [
797 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
801 * Validate a change of authentication data (e.g. passwords)
802 * @param AuthenticationRequest $req
803 * @param bool $checkData If false, $req hasn't been loaded from the
804 * submission so checks on user-submitted fields should be skipped. $req->username is
805 * considered user-submitted for this purpose, even if it cannot be changed via
806 * $req->loadFromSubmission.
809 public function allowsAuthenticationDataChange( AuthenticationRequest
$req, $checkData = true ) {
811 $providers = $this->getPrimaryAuthenticationProviders() +
812 $this->getSecondaryAuthenticationProviders();
813 foreach ( $providers as $provider ) {
814 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
815 if ( !$status->isGood() ) {
816 return Status
::wrap( $status );
818 $any = $any ||
$status->value
!== 'ignored';
821 $status = Status
::newGood( 'ignored' );
822 $status->warning( 'authmanager-change-not-supported' );
825 return Status
::newGood();
829 * Change authentication data (e.g. passwords)
831 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
832 * result in a successful login in the future.
834 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
835 * no longer result in a successful login.
837 * @param AuthenticationRequest $req
839 public function changeAuthenticationData( AuthenticationRequest
$req ) {
840 $this->logger
->info( 'Changing authentication data for {user} class {what}', [
841 'user' => is_string( $req->username
) ?
$req->username
: '<no name>',
842 'what' => get_class( $req ),
845 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
847 // When the main account's authentication data is changed, invalidate
848 // all BotPasswords too.
849 \BotPassword
::invalidateAllPasswordsForUser( $req->username
);
855 * @name Account creation
860 * Determine whether accounts can be created
863 public function canCreateAccounts() {
864 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
865 switch ( $provider->accountCreationType() ) {
866 case PrimaryAuthenticationProvider
::TYPE_CREATE
:
867 case PrimaryAuthenticationProvider
::TYPE_LINK
:
875 * Determine whether a particular account can be created
876 * @param string $username
877 * @param int $flags Bitfield of User:READ_* constants
880 public function canCreateAccount( $username, $flags = User
::READ_NORMAL
) {
881 if ( !$this->canCreateAccounts() ) {
882 return Status
::newFatal( 'authmanager-create-disabled' );
885 if ( $this->userExists( $username, $flags ) ) {
886 return Status
::newFatal( 'userexists' );
889 $user = User
::newFromName( $username, 'creatable' );
890 if ( !is_object( $user ) ) {
891 return Status
::newFatal( 'noname' );
893 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
894 if ( $user->getId() !== 0 ) {
895 return Status
::newFatal( 'userexists' );
899 // Denied by providers?
900 $providers = $this->getPreAuthenticationProviders() +
901 $this->getPrimaryAuthenticationProviders() +
902 $this->getSecondaryAuthenticationProviders();
903 foreach ( $providers as $provider ) {
904 $status = $provider->testUserForCreation( $user, false );
905 if ( !$status->isGood() ) {
906 return Status
::wrap( $status );
910 return Status
::newGood();
914 * Basic permissions checks on whether a user can create accounts
915 * @param User $creator User doing the account creation
918 public function checkAccountCreatePermissions( User
$creator ) {
919 // Wiki is read-only?
920 if ( wfReadOnly() ) {
921 return Status
::newFatal( 'readonlytext', wfReadOnlyReason() );
924 // This is awful, this permission check really shouldn't go through Title.
925 $permErrors = \SpecialPage
::getTitleFor( 'CreateAccount' )
926 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
928 $status = Status
::newGood();
929 foreach ( $permErrors as $args ) {
930 call_user_func_array( [ $status, 'fatal' ], $args );
935 $block = $creator->isBlockedFromCreateAccount();
939 $block->mReason ?
: wfMessage( 'blockednoreason' )->text(),
943 if ( $block->getType() === \Block
::TYPE_RANGE
) {
944 $errorMessage = 'cantcreateaccount-range-text';
945 $errorParams[] = $this->getRequest()->getIP();
947 $errorMessage = 'cantcreateaccount-text';
950 return Status
::newFatal( wfMessage( $errorMessage, $errorParams ) );
953 $ip = $this->getRequest()->getIP();
954 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
955 return Status
::newFatal( 'sorbs_create_account_reason' );
958 return Status
::newGood();
962 * Start an account creation flow
964 * In addition to the AuthenticationRequests returned by
965 * $this->getAuthenticationRequests(), a client might include a
966 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
968 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
970 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
971 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
972 * username set, that username must be used for all other requests.
974 * @param User $creator User doing the account creation
975 * @param AuthenticationRequest[] $reqs
976 * @param string $returnToUrl Url that REDIRECT responses should eventually
978 * @return AuthenticationResponse
980 public function beginAccountCreation( User
$creator, array $reqs, $returnToUrl ) {
981 $session = $this->request
->getSession();
982 if ( !$this->canCreateAccounts() ) {
983 // Caller should have called canCreateAccounts()
984 $session->remove( 'AuthManager::accountCreationState' );
985 throw new \
LogicException( 'Account creation is not possible' );
989 $username = AuthenticationRequest
::getUsernameFromRequests( $reqs );
990 } catch ( \UnexpectedValueException
$ex ) {
993 if ( $username === null ) {
994 $this->logger
->debug( __METHOD__
. ': No username provided' );
995 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
999 $status = $this->checkAccountCreatePermissions( $creator );
1000 if ( !$status->isGood() ) {
1001 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1002 'user' => $username,
1003 'creator' => $creator->getName(),
1004 'reason' => $status->getWikiText( null, null, 'en' )
1006 return AuthenticationResponse
::newFail( $status->getMessage() );
1009 $status = $this->canCreateAccount( $username, User
::READ_LOCKING
);
1010 if ( !$status->isGood() ) {
1011 $this->logger
->debug( __METHOD__
. ': {user} cannot be created: {reason}', [
1012 'user' => $username,
1013 'creator' => $creator->getName(),
1014 'reason' => $status->getWikiText( null, null, 'en' )
1016 return AuthenticationResponse
::newFail( $status->getMessage() );
1019 $user = User
::newFromName( $username, 'creatable' );
1020 foreach ( $reqs as $req ) {
1021 $req->username
= $username;
1022 $req->returnToUrl
= $returnToUrl;
1023 if ( $req instanceof UserDataAuthenticationRequest
) {
1024 $status = $req->populateUser( $user );
1025 if ( !$status->isGood() ) {
1026 $status = Status
::wrap( $status );
1027 $session->remove( 'AuthManager::accountCreationState' );
1028 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1029 'user' => $user->getName(),
1030 'creator' => $creator->getName(),
1031 'reason' => $status->getWikiText( null, null, 'en' ),
1033 return AuthenticationResponse
::newFail( $status->getMessage() );
1038 $this->removeAuthenticationSessionData( null );
1041 'username' => $username,
1043 'creatorid' => $creator->getId(),
1044 'creatorname' => $creator->getName(),
1046 'returnToUrl' => $returnToUrl,
1048 'primaryResponse' => null,
1050 'continueRequests' => [],
1052 'ranPreTests' => false,
1055 // Special case: converting a login to an account creation
1056 $req = AuthenticationRequest
::getRequestByClass(
1057 $reqs, CreateFromLoginAuthenticationRequest
::class
1060 $state['maybeLink'] = $req->maybeLink
;
1062 if ( $req->createRequest
) {
1063 $reqs[] = $req->createRequest
;
1064 $state['reqs'][] = $req->createRequest
;
1068 $session->setSecret( 'AuthManager::accountCreationState', $state );
1069 $session->persist();
1071 return $this->continueAccountCreation( $reqs );
1075 * Continue an account creation flow
1076 * @param AuthenticationRequest[] $reqs
1077 * @return AuthenticationResponse
1079 public function continueAccountCreation( array $reqs ) {
1080 $session = $this->request
->getSession();
1082 if ( !$this->canCreateAccounts() ) {
1083 // Caller should have called canCreateAccounts()
1084 $session->remove( 'AuthManager::accountCreationState' );
1085 throw new \
LogicException( 'Account creation is not possible' );
1088 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1089 if ( !is_array( $state ) ) {
1090 return AuthenticationResponse
::newFail(
1091 wfMessage( 'authmanager-create-not-in-progress' )
1094 $state['continueRequests'] = [];
1096 // Step 0: Prepare and validate the input
1098 $user = User
::newFromName( $state['username'], 'creatable' );
1099 if ( !is_object( $user ) ) {
1100 $session->remove( 'AuthManager::accountCreationState' );
1101 $this->logger
->debug( __METHOD__
. ': Invalid username', [
1102 'user' => $state['username'],
1104 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1107 if ( $state['creatorid'] ) {
1108 $creator = User
::newFromId( $state['creatorid'] );
1110 $creator = new User
;
1111 $creator->setName( $state['creatorname'] );
1114 // Avoid account creation races on double submissions
1115 $cache = \ObjectCache
::getLocalClusterInstance();
1116 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1118 // Don't clear AuthManager::accountCreationState for this code
1119 // path because the process that won the race owns it.
1120 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1121 'user' => $user->getName(),
1122 'creator' => $creator->getName(),
1124 return AuthenticationResponse
::newFail( wfMessage( 'usernameinprogress' ) );
1127 // Permissions check
1128 $status = $this->checkAccountCreatePermissions( $creator );
1129 if ( !$status->isGood() ) {
1130 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1131 'user' => $user->getName(),
1132 'creator' => $creator->getName(),
1133 'reason' => $status->getWikiText( null, null, 'en' )
1135 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1136 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1137 $session->remove( 'AuthManager::accountCreationState' );
1141 // Load from master for existence check
1142 $user->load( User
::READ_LOCKING
);
1144 if ( $state['userid'] === 0 ) {
1145 if ( $user->getId() != 0 ) {
1146 $this->logger
->debug( __METHOD__
. ': User exists locally', [
1147 'user' => $user->getName(),
1148 'creator' => $creator->getName(),
1150 $ret = AuthenticationResponse
::newFail( wfMessage( 'userexists' ) );
1151 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1152 $session->remove( 'AuthManager::accountCreationState' );
1156 if ( $user->getId() == 0 ) {
1157 $this->logger
->debug( __METHOD__
. ': User does not exist locally when it should', [
1158 'user' => $user->getName(),
1159 'creator' => $creator->getName(),
1160 'expected_id' => $state['userid'],
1162 throw new \
UnexpectedValueException(
1163 "User \"{$state['username']}\" should exist now, but doesn't!"
1166 if ( $user->getId() != $state['userid'] ) {
1167 $this->logger
->debug( __METHOD__
. ': User ID/name mismatch', [
1168 'user' => $user->getName(),
1169 'creator' => $creator->getName(),
1170 'expected_id' => $state['userid'],
1171 'actual_id' => $user->getId(),
1173 throw new \
UnexpectedValueException(
1174 "User \"{$state['username']}\" exists, but " .
1175 "ID {$user->getId()} != {$state['userid']}!"
1179 foreach ( $state['reqs'] as $req ) {
1180 if ( $req instanceof UserDataAuthenticationRequest
) {
1181 $status = $req->populateUser( $user );
1182 if ( !$status->isGood() ) {
1183 // This should never happen...
1184 $status = Status
::wrap( $status );
1185 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1186 'user' => $user->getName(),
1187 'creator' => $creator->getName(),
1188 'reason' => $status->getWikiText( null, null, 'en' ),
1190 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1191 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1192 $session->remove( 'AuthManager::accountCreationState' );
1198 foreach ( $reqs as $req ) {
1199 $req->returnToUrl
= $state['returnToUrl'];
1200 $req->username
= $state['username'];
1203 // Run pre-creation tests, if we haven't already
1204 if ( !$state['ranPreTests'] ) {
1205 $providers = $this->getPreAuthenticationProviders() +
1206 $this->getPrimaryAuthenticationProviders() +
1207 $this->getSecondaryAuthenticationProviders();
1208 foreach ( $providers as $id => $provider ) {
1209 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1210 if ( !$status->isGood() ) {
1211 $this->logger
->debug( __METHOD__
. ": Fail in pre-authentication by $id", [
1212 'user' => $user->getName(),
1213 'creator' => $creator->getName(),
1215 $ret = AuthenticationResponse
::newFail(
1216 Status
::wrap( $status )->getMessage()
1218 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1219 $session->remove( 'AuthManager::accountCreationState' );
1224 $state['ranPreTests'] = true;
1227 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1229 if ( $state['primary'] === null ) {
1230 // We haven't picked a PrimaryAuthenticationProvider yet
1231 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1232 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_NONE
) {
1235 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1236 switch ( $res->status
) {
1237 case AuthenticationResponse
::PASS
;
1238 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1239 'user' => $user->getName(),
1240 'creator' => $creator->getName(),
1242 $state['primary'] = $id;
1243 $state['primaryResponse'] = $res;
1245 case AuthenticationResponse
::FAIL
;
1246 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1247 'user' => $user->getName(),
1248 'creator' => $creator->getName(),
1250 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1251 $session->remove( 'AuthManager::accountCreationState' );
1253 case AuthenticationResponse
::ABSTAIN
;
1256 case AuthenticationResponse
::REDIRECT
;
1257 case AuthenticationResponse
::UI
;
1258 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1259 'user' => $user->getName(),
1260 'creator' => $creator->getName(),
1262 $state['primary'] = $id;
1263 $state['continueRequests'] = $res->neededRequests
;
1264 $session->setSecret( 'AuthManager::accountCreationState', $state );
1267 // @codeCoverageIgnoreStart
1269 throw new \
DomainException(
1270 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1272 // @codeCoverageIgnoreEnd
1275 if ( $state['primary'] === null ) {
1276 $this->logger
->debug( __METHOD__
. ': Primary creation failed because no provider accepted', [
1277 'user' => $user->getName(),
1278 'creator' => $creator->getName(),
1280 $ret = AuthenticationResponse
::newFail(
1281 wfMessage( 'authmanager-create-no-primary' )
1283 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1284 $session->remove( 'AuthManager::accountCreationState' );
1287 } elseif ( $state['primaryResponse'] === null ) {
1288 $provider = $this->getAuthenticationProvider( $state['primary'] );
1289 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1290 // Configuration changed? Force them to start over.
1291 // @codeCoverageIgnoreStart
1292 $ret = AuthenticationResponse
::newFail(
1293 wfMessage( 'authmanager-create-not-in-progress' )
1295 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1296 $session->remove( 'AuthManager::accountCreationState' );
1298 // @codeCoverageIgnoreEnd
1300 $id = $provider->getUniqueId();
1301 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1302 switch ( $res->status
) {
1303 case AuthenticationResponse
::PASS
;
1304 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1305 'user' => $user->getName(),
1306 'creator' => $creator->getName(),
1308 $state['primaryResponse'] = $res;
1310 case AuthenticationResponse
::FAIL
;
1311 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1312 'user' => $user->getName(),
1313 'creator' => $creator->getName(),
1315 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1316 $session->remove( 'AuthManager::accountCreationState' );
1318 case AuthenticationResponse
::REDIRECT
;
1319 case AuthenticationResponse
::UI
;
1320 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1321 'user' => $user->getName(),
1322 'creator' => $creator->getName(),
1324 $state['continueRequests'] = $res->neededRequests
;
1325 $session->setSecret( 'AuthManager::accountCreationState', $state );
1328 throw new \
DomainException(
1329 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1334 // Step 2: Primary authentication succeeded, create the User object
1335 // and add the user locally.
1337 if ( $state['userid'] === 0 ) {
1338 $this->logger
->info( 'Creating user {user} during account creation', [
1339 'user' => $user->getName(),
1340 'creator' => $creator->getName(),
1342 $status = $user->addToDatabase();
1343 if ( !$status->isOk() ) {
1344 // @codeCoverageIgnoreStart
1345 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1346 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1347 $session->remove( 'AuthManager::accountCreationState' );
1349 // @codeCoverageIgnoreEnd
1351 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1352 \Hooks
::run( 'LocalUserCreated', [ $user, false ] );
1353 $user->saveSettings();
1354 $state['userid'] = $user->getId();
1356 // Update user count
1357 \DeferredUpdates
::addUpdate( new \
SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1359 // Watch user's userpage and talk page
1360 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1362 // Inform the provider
1363 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1366 if ( $this->config
->get( 'NewUserLog' ) ) {
1367 $isAnon = $creator->isAnon();
1368 $logEntry = new \
ManualLogEntry(
1370 $logSubtype ?
: ( $isAnon ?
'create' : 'create2' )
1372 $logEntry->setPerformer( $isAnon ?
$user : $creator );
1373 $logEntry->setTarget( $user->getUserPage() );
1374 $req = AuthenticationRequest
::getRequestByClass(
1375 $state['reqs'], CreationReasonAuthenticationRequest
::class
1377 $logEntry->setComment( $req ?
$req->reason
: '' );
1378 $logEntry->setParameters( [
1379 '4::userid' => $user->getId(),
1381 $logid = $logEntry->insert();
1382 $logEntry->publish( $logid );
1386 // Step 3: Iterate over all the secondary authentication providers.
1388 $beginReqs = $state['reqs'];
1390 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1391 if ( !isset( $state['secondary'][$id] ) ) {
1392 // This provider isn't started yet, so we pass it the set
1393 // of reqs from beginAuthentication instead of whatever
1394 // might have been used by a previous provider in line.
1395 $func = 'beginSecondaryAccountCreation';
1396 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1397 } elseif ( !$state['secondary'][$id] ) {
1398 $func = 'continueSecondaryAccountCreation';
1399 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1403 switch ( $res->status
) {
1404 case AuthenticationResponse
::PASS
;
1405 $this->logger
->debug( __METHOD__
. ": Secondary creation passed by $id", [
1406 'user' => $user->getName(),
1407 'creator' => $creator->getName(),
1410 case AuthenticationResponse
::ABSTAIN
;
1411 $state['secondary'][$id] = true;
1413 case AuthenticationResponse
::REDIRECT
;
1414 case AuthenticationResponse
::UI
;
1415 $this->logger
->debug( __METHOD__
. ": Secondary creation $res->status by $id", [
1416 'user' => $user->getName(),
1417 'creator' => $creator->getName(),
1419 $state['secondary'][$id] = false;
1420 $state['continueRequests'] = $res->neededRequests
;
1421 $session->setSecret( 'AuthManager::accountCreationState', $state );
1423 case AuthenticationResponse
::FAIL
;
1424 throw new \
DomainException(
1425 get_class( $provider ) . "::{$func}() returned $res->status." .
1426 ' Secondary providers are not allowed to fail account creation, that' .
1427 ' should have been done via testForAccountCreation().'
1429 // @codeCoverageIgnoreStart
1431 throw new \
DomainException(
1432 get_class( $provider ) . "::{$func}() returned $res->status"
1434 // @codeCoverageIgnoreEnd
1438 $id = $user->getId();
1439 $name = $user->getName();
1440 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1441 $ret = AuthenticationResponse
::newPass( $name );
1442 $ret->loginRequest
= $req;
1443 $this->createdAccountAuthenticationRequests
[] = $req;
1445 $this->logger
->info( __METHOD__
. ': Account creation succeeded for {user}', [
1446 'user' => $user->getName(),
1447 'creator' => $creator->getName(),
1450 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1451 $session->remove( 'AuthManager::accountCreationState' );
1452 $this->removeAuthenticationSessionData( null );
1454 } catch ( \Exception
$ex ) {
1455 $session->remove( 'AuthManager::accountCreationState' );
1461 * Auto-create an account, and log into that account
1462 * @param User $user User to auto-create
1463 * @param string $source What caused the auto-creation? This must be the ID
1464 * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1465 * @param bool $login Whether to also log the user in
1466 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1468 public function autoCreateUser( User
$user, $source, $login = true ) {
1469 if ( $source !== self
::AUTOCREATE_SOURCE_SESSION
&&
1470 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1472 throw new \
InvalidArgumentException( "Unknown auto-creation source: $source" );
1475 $username = $user->getName();
1477 // Try the local user from the slave DB
1478 $localId = User
::idFromName( $username );
1479 $flags = User
::READ_NORMAL
;
1481 // Fetch the user ID from the master, so that we don't try to create the user
1482 // when they already exist, due to replication lag
1483 // @codeCoverageIgnoreStart
1484 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1485 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1486 $flags = User
::READ_LATEST
;
1488 // @codeCoverageIgnoreEnd
1491 $this->logger
->debug( __METHOD__
. ': {username} already exists locally', [
1492 'username' => $username,
1494 $user->setId( $localId );
1495 $user->loadFromId( $flags );
1497 $this->setSessionDataForUser( $user );
1499 $status = Status
::newGood();
1500 $status->warning( 'userexists' );
1504 // Wiki is read-only?
1505 if ( wfReadOnly() ) {
1506 $this->logger
->debug( __METHOD__
. ': denied by wfReadOnly(): {reason}', [
1507 'username' => $username,
1508 'reason' => wfReadOnlyReason(),
1511 $user->loadFromId();
1512 return Status
::newFatal( 'readonlytext', wfReadOnlyReason() );
1515 // Check the session, if we tried to create this user already there's
1516 // no point in retrying.
1517 $session = $this->request
->getSession();
1518 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1519 $this->logger
->debug( __METHOD__
. ': blacklisted in session {sessionid}', [
1520 'username' => $username,
1521 'sessionid' => $session->getId(),
1524 $user->loadFromId();
1525 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1526 if ( $reason instanceof StatusValue
) {
1527 return Status
::wrap( $reason );
1529 return Status
::newFatal( $reason );
1533 // Is the username creatable?
1534 if ( !User
::isCreatableName( $username ) ) {
1535 $this->logger
->debug( __METHOD__
. ': name "{username}" is not creatable', [
1536 'username' => $username,
1538 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
1540 $user->loadFromId();
1541 return Status
::newFatal( 'noname' );
1544 // Is the IP user able to create accounts?
1546 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1547 $this->logger
->debug( __METHOD__
. ': IP lacks the ability to create or autocreate accounts', [
1548 'username' => $username,
1549 'ip' => $anon->getName(),
1551 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
1552 $session->persist();
1554 $user->loadFromId();
1555 return Status
::newFatal( 'authmanager-autocreate-noperm' );
1558 // Avoid account creation races on double submissions
1559 $cache = \ObjectCache
::getLocalClusterInstance();
1560 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1562 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1563 'user' => $username,
1566 $user->loadFromId();
1567 return Status
::newFatal( 'usernameinprogress' );
1570 // Denied by providers?
1571 $providers = $this->getPreAuthenticationProviders() +
1572 $this->getPrimaryAuthenticationProviders() +
1573 $this->getSecondaryAuthenticationProviders();
1574 foreach ( $providers as $provider ) {
1575 $status = $provider->testUserForCreation( $user, $source );
1576 if ( !$status->isGood() ) {
1577 $ret = Status
::wrap( $status );
1578 $this->logger
->debug( __METHOD__
. ': Provider denied creation of {username}: {reason}', [
1579 'username' => $username,
1580 'reason' => $ret->getWikiText( null, null, 'en' ),
1582 $session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
1584 $user->loadFromId();
1589 // Ignore warnings about master connections/writes...hard to avoid here
1590 \Profiler
::instance()->getTransactionProfiler()->resetExpectations();
1592 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1593 if ( $cache->get( $backoffKey ) ) {
1594 $this->logger
->debug( __METHOD__
. ': {username} denied by prior creation attempt failures', [
1595 'username' => $username,
1598 $user->loadFromId();
1599 return Status
::newFatal( 'authmanager-autocreate-exception' );
1602 // Checks passed, create the user...
1603 $from = isset( $_SERVER['REQUEST_URI'] ) ?
$_SERVER['REQUEST_URI'] : 'CLI';
1604 $this->logger
->info( __METHOD__
. ': creating new user ({username}) - from: {from}', [
1605 'username' => $username,
1610 $status = $user->addToDatabase();
1611 if ( !$status->isOk() ) {
1612 // double-check for a race condition (T70012)
1613 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1615 $this->logger
->info( __METHOD__
. ': {username} already exists locally (race)', [
1616 'username' => $username,
1618 $user->setId( $localId );
1619 $user->loadFromId( User
::READ_LATEST
);
1621 $this->setSessionDataForUser( $user );
1623 $status = Status
::newGood();
1624 $status->warning( 'userexists' );
1626 $this->logger
->error( __METHOD__
. ': {username} failed with message {message}', [
1627 'username' => $username,
1628 'message' => $status->getWikiText( null, null, 'en' )
1631 $user->loadFromId();
1635 } catch ( \Exception
$ex ) {
1636 $this->logger
->error( __METHOD__
. ': {username} failed with exception {exception}', [
1637 'username' => $username,
1640 // Do not keep throwing errors for a while
1641 $cache->set( $backoffKey, 1, 600 );
1642 // Bubble up error; which should normally trigger DB rollbacks
1646 $this->setDefaultUserOptions( $user, true );
1648 // Inform the providers
1649 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1651 \Hooks
::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1652 \Hooks
::run( 'LocalUserCreated', [ $user, true ] );
1653 $user->saveSettings();
1655 // Update user count
1656 \DeferredUpdates
::addUpdate( new \
SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1658 // Watch user's userpage and talk page
1659 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1662 if ( $this->config
->get( 'NewUserLog' ) ) {
1663 $logEntry = new \
ManualLogEntry( 'newusers', 'autocreate' );
1664 $logEntry->setPerformer( $user );
1665 $logEntry->setTarget( $user->getUserPage() );
1666 $logEntry->setComment( '' );
1667 $logEntry->setParameters( [
1668 '4::userid' => $user->getId(),
1670 $logid = $logEntry->insert();
1674 $this->setSessionDataForUser( $user );
1677 return Status
::newGood();
1683 * @name Account linking
1688 * Determine whether accounts can be linked
1691 public function canLinkAccounts() {
1692 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1693 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
) {
1701 * Start an account linking flow
1703 * @param User $user User being linked
1704 * @param AuthenticationRequest[] $reqs
1705 * @param string $returnToUrl Url that REDIRECT responses should eventually
1707 * @return AuthenticationResponse
1709 public function beginAccountLink( User
$user, array $reqs, $returnToUrl ) {
1710 $session = $this->request
->getSession();
1711 $session->remove( 'AuthManager::accountLinkState' );
1713 if ( !$this->canLinkAccounts() ) {
1714 // Caller should have called canLinkAccounts()
1715 throw new \
LogicException( 'Account linking is not possible' );
1718 if ( $user->getId() === 0 ) {
1719 if ( !User
::isUsableName( $user->getName() ) ) {
1720 $msg = wfMessage( 'noname' );
1722 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1724 return AuthenticationResponse
::newFail( $msg );
1726 foreach ( $reqs as $req ) {
1727 $req->username
= $user->getName();
1728 $req->returnToUrl
= $returnToUrl;
1731 $this->removeAuthenticationSessionData( null );
1733 $providers = $this->getPreAuthenticationProviders();
1734 foreach ( $providers as $id => $provider ) {
1735 $status = $provider->testForAccountLink( $user );
1736 if ( !$status->isGood() ) {
1737 $this->logger
->debug( __METHOD__
. ": Account linking pre-check failed by $id", [
1738 'user' => $user->getName(),
1740 $ret = AuthenticationResponse
::newFail(
1741 Status
::wrap( $status )->getMessage()
1743 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1749 'username' => $user->getName(),
1750 'userid' => $user->getId(),
1751 'returnToUrl' => $returnToUrl,
1753 'continueRequests' => [],
1756 $providers = $this->getPrimaryAuthenticationProviders();
1757 foreach ( $providers as $id => $provider ) {
1758 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider
::TYPE_LINK
) {
1762 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1763 switch ( $res->status
) {
1764 case AuthenticationResponse
::PASS
;
1765 $this->logger
->info( "Account linked to {user} by $id", [
1766 'user' => $user->getName(),
1768 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1771 case AuthenticationResponse
::FAIL
;
1772 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1773 'user' => $user->getName(),
1775 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1778 case AuthenticationResponse
::ABSTAIN
;
1782 case AuthenticationResponse
::REDIRECT
;
1783 case AuthenticationResponse
::UI
;
1784 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1785 'user' => $user->getName(),
1787 $state['primary'] = $id;
1788 $state['continueRequests'] = $res->neededRequests
;
1789 $session->setSecret( 'AuthManager::accountLinkState', $state );
1790 $session->persist();
1793 // @codeCoverageIgnoreStart
1795 throw new \
DomainException(
1796 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1798 // @codeCoverageIgnoreEnd
1802 $this->logger
->debug( __METHOD__
. ': Account linking failed because no provider accepted', [
1803 'user' => $user->getName(),
1805 $ret = AuthenticationResponse
::newFail(
1806 wfMessage( 'authmanager-link-no-primary' )
1808 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1813 * Continue an account linking flow
1814 * @param AuthenticationRequest[] $reqs
1815 * @return AuthenticationResponse
1817 public function continueAccountLink( array $reqs ) {
1818 $session = $this->request
->getSession();
1820 if ( !$this->canLinkAccounts() ) {
1821 // Caller should have called canLinkAccounts()
1822 $session->remove( 'AuthManager::accountLinkState' );
1823 throw new \
LogicException( 'Account linking is not possible' );
1826 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1827 if ( !is_array( $state ) ) {
1828 return AuthenticationResponse
::newFail(
1829 wfMessage( 'authmanager-link-not-in-progress' )
1832 $state['continueRequests'] = [];
1834 // Step 0: Prepare and validate the input
1836 $user = User
::newFromName( $state['username'], 'usable' );
1837 if ( !is_object( $user ) ) {
1838 $session->remove( 'AuthManager::accountLinkState' );
1839 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1841 if ( $user->getId() != $state['userid'] ) {
1842 throw new \
UnexpectedValueException(
1843 "User \"{$state['username']}\" is valid, but " .
1844 "ID {$user->getId()} != {$state['userid']}!"
1848 foreach ( $reqs as $req ) {
1849 $req->username
= $state['username'];
1850 $req->returnToUrl
= $state['returnToUrl'];
1853 // Step 1: Call the primary again until it succeeds
1855 $provider = $this->getAuthenticationProvider( $state['primary'] );
1856 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1857 // Configuration changed? Force them to start over.
1858 // @codeCoverageIgnoreStart
1859 $ret = AuthenticationResponse
::newFail(
1860 wfMessage( 'authmanager-link-not-in-progress' )
1862 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1863 $session->remove( 'AuthManager::accountLinkState' );
1865 // @codeCoverageIgnoreEnd
1867 $id = $provider->getUniqueId();
1868 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1869 switch ( $res->status
) {
1870 case AuthenticationResponse
::PASS
;
1871 $this->logger
->info( "Account linked to {user} by $id", [
1872 'user' => $user->getName(),
1874 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1875 $session->remove( 'AuthManager::accountLinkState' );
1877 case AuthenticationResponse
::FAIL
;
1878 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1879 'user' => $user->getName(),
1881 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1882 $session->remove( 'AuthManager::accountLinkState' );
1884 case AuthenticationResponse
::REDIRECT
;
1885 case AuthenticationResponse
::UI
;
1886 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1887 'user' => $user->getName(),
1889 $state['continueRequests'] = $res->neededRequests
;
1890 $session->setSecret( 'AuthManager::accountLinkState', $state );
1893 throw new \
DomainException(
1894 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1897 } catch ( \Exception
$ex ) {
1898 $session->remove( 'AuthManager::accountLinkState' );
1906 * @name Information methods
1911 * Return the applicable list of AuthenticationRequests
1913 * Possible values for $action:
1914 * - ACTION_LOGIN: Valid for passing to beginAuthentication
1915 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1916 * - ACTION_CREATE: Valid for passing to beginAccountCreation
1917 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1918 * - ACTION_LINK: Valid for passing to beginAccountLink
1919 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1920 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1921 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1922 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
1924 * @param string $action One of the AuthManager::ACTION_* constants
1925 * @param User|null $user User being acted on, instead of the current user.
1926 * @return AuthenticationRequest[]
1928 public function getAuthenticationRequests( $action, User
$user = null ) {
1930 $providerAction = $action;
1932 // Figure out which providers to query
1933 switch ( $action ) {
1934 case self
::ACTION_LOGIN
:
1935 case self
::ACTION_CREATE
:
1936 $providers = $this->getPreAuthenticationProviders() +
1937 $this->getPrimaryAuthenticationProviders() +
1938 $this->getSecondaryAuthenticationProviders();
1941 case self
::ACTION_LOGIN_CONTINUE
:
1942 $state = $this->request
->getSession()->getSecret( 'AuthManager::authnState' );
1943 return is_array( $state ) ?
$state['continueRequests'] : [];
1945 case self
::ACTION_CREATE_CONTINUE
:
1946 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
1947 return is_array( $state ) ?
$state['continueRequests'] : [];
1949 case self
::ACTION_LINK
:
1950 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1951 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
1955 case self
::ACTION_UNLINK
:
1956 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1957 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
1960 // To providers, unlink and remove are identical.
1961 $providerAction = self
::ACTION_REMOVE
;
1964 case self
::ACTION_LINK_CONTINUE
:
1965 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' );
1966 return is_array( $state ) ?
$state['continueRequests'] : [];
1968 case self
::ACTION_CHANGE
:
1969 case self
::ACTION_REMOVE
:
1970 $providers = $this->getPrimaryAuthenticationProviders() +
1971 $this->getSecondaryAuthenticationProviders();
1974 // @codeCoverageIgnoreStart
1976 throw new \
DomainException( __METHOD__
. ": Invalid action \"$action\"" );
1978 // @codeCoverageIgnoreEnd
1980 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
1984 * Internal request lookup for self::getAuthenticationRequests
1986 * @param string $providerAction Action to pass to providers
1987 * @param array $options Options to pass to providers
1988 * @param AuthenticationProvider[] $providers
1989 * @param User|null $user
1990 * @return AuthenticationRequest[]
1992 private function getAuthenticationRequestsInternal(
1993 $providerAction, array $options, array $providers, User
$user = null
1995 $user = $user ?
: \RequestContext
::getMain()->getUser();
1996 $options['username'] = $user->isAnon() ?
null : $user->getName();
1998 // Query them and merge results
2000 $allPrimaryRequired = null;
2001 foreach ( $providers as $provider ) {
2002 $isPrimary = $provider instanceof PrimaryAuthenticationProvider
;
2004 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2005 $id = $req->getUniqueId();
2007 // If it's from a Primary, mark it as "primary-required" but
2008 // track it for later.
2010 if ( $req->required
) {
2011 $thisRequired[$id] = true;
2012 $req->required
= AuthenticationRequest
::PRIMARY_REQUIRED
;
2016 if ( !isset( $reqs[$id] ) ||
$req->required
=== AuthenticationRequest
::REQUIRED
) {
2021 // Track which requests are required by all primaries
2023 $allPrimaryRequired = $allPrimaryRequired === null
2025 : array_intersect_key( $allPrimaryRequired, $thisRequired );
2028 // Any requests that were required by all primaries are required.
2029 foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
2030 $reqs[$id]->required
= AuthenticationRequest
::REQUIRED
;
2033 // AuthManager has its own req for some actions
2034 switch ( $providerAction ) {
2035 case self
::ACTION_LOGIN
:
2036 $reqs[] = new RememberMeAuthenticationRequest
;
2039 case self
::ACTION_CREATE
:
2040 $reqs[] = new UsernameAuthenticationRequest
;
2041 $reqs[] = new UserDataAuthenticationRequest
;
2042 if ( $options['username'] !== null ) {
2043 $reqs[] = new CreationReasonAuthenticationRequest
;
2044 $options['username'] = null; // Don't fill in the username below
2049 // Fill in reqs data
2050 foreach ( $reqs as $req ) {
2051 $req->action
= $providerAction;
2052 if ( $req->username
=== null ) {
2053 $req->username
= $options['username'];
2057 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2058 if ( $providerAction === self
::ACTION_CHANGE ||
$providerAction === self
::ACTION_REMOVE
) {
2059 $reqs = array_filter( $reqs, function ( $req ) {
2060 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2064 return array_values( $reqs );
2068 * Determine whether a username exists
2069 * @param string $username
2070 * @param int $flags Bitfield of User:READ_* constants
2073 public function userExists( $username, $flags = User
::READ_NORMAL
) {
2074 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2075 if ( $provider->testUserExists( $username, $flags ) ) {
2084 * Determine whether a user property should be allowed to be changed.
2086 * Supported properties are:
2091 * @param string $property
2094 public function allowsPropertyChange( $property ) {
2095 $providers = $this->getPrimaryAuthenticationProviders() +
2096 $this->getSecondaryAuthenticationProviders();
2097 foreach ( $providers as $provider ) {
2098 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2106 * Get a provider by ID
2107 * @note This is public so extensions can check whether their own provider
2108 * is installed and so they can read its configuration if necessary.
2109 * Other uses are not recommended.
2111 * @return AuthenticationProvider|null
2113 public function getAuthenticationProvider( $id ) {
2115 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2116 return $this->allAuthenticationProviders
[$id];
2119 // Slow version: instantiate each kind and check
2120 $providers = $this->getPrimaryAuthenticationProviders();
2121 if ( isset( $providers[$id] ) ) {
2122 return $providers[$id];
2124 $providers = $this->getSecondaryAuthenticationProviders();
2125 if ( isset( $providers[$id] ) ) {
2126 return $providers[$id];
2128 $providers = $this->getPreAuthenticationProviders();
2129 if ( isset( $providers[$id] ) ) {
2130 return $providers[$id];
2139 * @name Internal methods
2144 * Store authentication in the current session
2145 * @protected For use by AuthenticationProviders
2146 * @param string $key
2147 * @param mixed $data Must be serializable
2149 public function setAuthenticationSessionData( $key, $data ) {
2150 $session = $this->request
->getSession();
2151 $arr = $session->getSecret( 'authData' );
2152 if ( !is_array( $arr ) ) {
2156 $session->setSecret( 'authData', $arr );
2160 * Fetch authentication data from the current session
2161 * @protected For use by AuthenticationProviders
2162 * @param string $key
2163 * @param mixed $default
2166 public function getAuthenticationSessionData( $key, $default = null ) {
2167 $arr = $this->request
->getSession()->getSecret( 'authData' );
2168 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2176 * Remove authentication data
2177 * @protected For use by AuthenticationProviders
2178 * @param string|null $key If null, all data is removed
2180 public function removeAuthenticationSessionData( $key ) {
2181 $session = $this->request
->getSession();
2182 if ( $key === null ) {
2183 $session->remove( 'authData' );
2185 $arr = $session->getSecret( 'authData' );
2186 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2187 unset( $arr[$key] );
2188 $session->setSecret( 'authData', $arr );
2194 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2195 * @param string $class
2196 * @param array[] $specs
2197 * @return AuthenticationProvider[]
2199 protected function providerArrayFromSpecs( $class, array $specs ) {
2201 foreach ( $specs as &$spec ) {
2202 $spec = [ 'sort2' => $i++
] +
$spec +
[ 'sort' => 0 ];
2205 usort( $specs, function ( $a, $b ) {
2206 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2207 ?
: $a['sort2'] - $b['sort2'];
2211 foreach ( $specs as $spec ) {
2212 $provider = \ObjectFactory
::getObjectFromSpec( $spec );
2213 if ( !$provider instanceof $class ) {
2214 throw new \
RuntimeException(
2215 "Expected instance of $class, got " . get_class( $provider )
2218 $provider->setLogger( $this->logger
);
2219 $provider->setManager( $this );
2220 $provider->setConfig( $this->config
);
2221 $id = $provider->getUniqueId();
2222 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2223 throw new \
RuntimeException(
2224 "Duplicate specifications for id $id (classes " .
2225 get_class( $provider ) . ' and ' .
2226 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
2229 $this->allAuthenticationProviders
[$id] = $provider;
2230 $ret[$id] = $provider;
2236 * Get the configuration
2239 private function getConfiguration() {
2240 return $this->config
->get( 'AuthManagerConfig' ) ?
: $this->config
->get( 'AuthManagerAutoConfig' );
2244 * Get the list of PreAuthenticationProviders
2245 * @return PreAuthenticationProvider[]
2247 protected function getPreAuthenticationProviders() {
2248 if ( $this->preAuthenticationProviders
=== null ) {
2249 $conf = $this->getConfiguration();
2250 $this->preAuthenticationProviders
= $this->providerArrayFromSpecs(
2251 PreAuthenticationProvider
::class, $conf['preauth']
2254 return $this->preAuthenticationProviders
;
2258 * Get the list of PrimaryAuthenticationProviders
2259 * @return PrimaryAuthenticationProvider[]
2261 protected function getPrimaryAuthenticationProviders() {
2262 if ( $this->primaryAuthenticationProviders
=== null ) {
2263 $conf = $this->getConfiguration();
2264 $this->primaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2265 PrimaryAuthenticationProvider
::class, $conf['primaryauth']
2268 return $this->primaryAuthenticationProviders
;
2272 * Get the list of SecondaryAuthenticationProviders
2273 * @return SecondaryAuthenticationProvider[]
2275 protected function getSecondaryAuthenticationProviders() {
2276 if ( $this->secondaryAuthenticationProviders
=== null ) {
2277 $conf = $this->getConfiguration();
2278 $this->secondaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2279 SecondaryAuthenticationProvider
::class, $conf['secondaryauth']
2282 return $this->secondaryAuthenticationProviders
;
2287 * @param bool|null $remember
2289 private function setSessionDataForUser( $user, $remember = null ) {
2290 $session = $this->request
->getSession();
2291 $delay = $session->delaySave();
2293 $session->resetId();
2294 $session->resetAllTokens();
2295 if ( $session->canSetUser() ) {
2296 $session->setUser( $user );
2298 if ( $remember !== null ) {
2299 $session->setRememberUser( $remember );
2301 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2302 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2303 $session->persist();
2305 \ScopedCallback
::consume( $delay );
2307 \Hooks
::run( 'UserLoggedIn', [ $user ] );
2312 * @param bool $useContextLang Use 'uselang' to set the user's language
2314 private function setDefaultUserOptions( User
$user, $useContextLang ) {
2317 \MediaWiki\Session\SessionManager
::singleton()->invalidateSessionsForUser( $user );
2319 $lang = $useContextLang ? \RequestContext
::getMain()->getLanguage() : $wgContLang;
2320 $user->setOption( 'language', $lang->getPreferredVariant() );
2322 if ( $wgContLang->hasVariants() ) {
2323 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2328 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2329 * @param string $method
2330 * @param array $args
2332 private function callMethodOnProviders( $which, $method, array $args ) {
2335 $providers +
= $this->getPreAuthenticationProviders();
2338 $providers +
= $this->getPrimaryAuthenticationProviders();
2341 $providers +
= $this->getSecondaryAuthenticationProviders();
2343 foreach ( $providers as $provider ) {
2344 call_user_func_array( [ $provider, $method ], $args );
2349 * Reset the internal caching for unit testing
2351 public static function resetCache() {
2352 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2353 // @codeCoverageIgnoreStart
2354 throw new \
MWException( __METHOD__
. ' may only be called from unit tests!' );
2355 // @codeCoverageIgnoreEnd
2358 self
::$instance = null;
2366 * For really cool vim folding this needs to be at the end:
2367 * vim: foldmarker=@{,@} foldmethod=marker