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 MediaWiki\Block\DatabaseBlock
;
28 use MediaWiki\MediaWikiServices
;
29 use Psr\Log\LoggerAwareInterface
;
30 use Psr\Log\LoggerInterface
;
35 use Wikimedia\ObjectFactory
;
38 * This serves as the entry point to the authentication system.
40 * In the future, it may also serve as the entry point to the authorization
43 * If you are looking at this because you are working on an extension that creates its own
44 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
45 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
46 * or the createaccount API. Trying to call this class directly will very likely end up in
47 * security vulnerabilities or broken UX in edge cases.
49 * If you are working on an extension that needs to integrate with the authentication system
50 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
51 * need to write an AuthenticationProvider.
53 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
54 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
55 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
56 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
57 * responsibility to ensure that the user can authenticate somehow (see especially
58 * PrimaryAuthenticationProvider::autoCreatedAccount()). The same functionality can also be used
59 * from Maintenance scripts such as createAndPromote.php.
60 * If you are writing code that is not associated with such a provider and needs to create accounts
61 * programmatically for real users, you should rethink your architecture. There is no good way to
62 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
63 * cannot provide any means for users to access the accounts it would create.
65 * The two main control flows when using this class are as follows:
66 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
67 * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
68 * exposing a form specification via the API, so that the client can build it), and pass them to
69 * the appropriate begin* method. That will return either a success/failure response, or more
70 * requests to fill (either by building a form or by redirecting the user to some external
71 * provider which will send the data back), in which case they need to be submitted to the
72 * appropriate continue* method and that step has to be repeated until the response is a success
73 * or failure response. AuthManager will use the session to maintain internal state during the
75 * * Code doing an authentication data change will call getAuthenticationRequests(), select
76 * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
77 * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
78 * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
83 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
85 class AuthManager
implements LoggerAwareInterface
{
86 /** Log in with an existing (not necessarily local) user */
87 const ACTION_LOGIN
= 'login';
88 /** Continue a login process that was interrupted by the need for user input or communication
89 * with an external provider */
90 const ACTION_LOGIN_CONTINUE
= 'login-continue';
91 /** Create a new user */
92 const ACTION_CREATE
= 'create';
93 /** Continue a user creation process that was interrupted by the need for user input or
94 * communication with an external provider */
95 const ACTION_CREATE_CONTINUE
= 'create-continue';
96 /** Link an existing user to a third-party account */
97 const ACTION_LINK
= 'link';
98 /** Continue a user linking process that was interrupted by the need for user input or
99 * communication with an external provider */
100 const ACTION_LINK_CONTINUE
= 'link-continue';
101 /** Change a user's credentials */
102 const ACTION_CHANGE
= 'change';
103 /** Remove a user's credentials */
104 const ACTION_REMOVE
= 'remove';
105 /** Like ACTION_REMOVE but for linking providers only */
106 const ACTION_UNLINK
= 'unlink';
108 /** Security-sensitive operations are ok. */
110 /** Security-sensitive operations should re-authenticate. */
111 const SEC_REAUTH
= 'reauth';
112 /** Security-sensitive should not be performed. */
113 const SEC_FAIL
= 'fail';
115 /** Auto-creation is due to SessionManager */
116 const AUTOCREATE_SOURCE_SESSION
= \MediaWiki\Session\SessionManager
::class;
118 /** Auto-creation is due to a Maintenance script */
119 const AUTOCREATE_SOURCE_MAINT
= '::Maintenance::';
121 /** @var AuthManager|null */
122 private static $instance = null;
124 /** @var WebRequest */
130 /** @var LoggerInterface */
133 /** @var AuthenticationProvider[] */
134 private $allAuthenticationProviders = [];
136 /** @var PreAuthenticationProvider[] */
137 private $preAuthenticationProviders = null;
139 /** @var PrimaryAuthenticationProvider[] */
140 private $primaryAuthenticationProviders = null;
142 /** @var SecondaryAuthenticationProvider[] */
143 private $secondaryAuthenticationProviders = null;
145 /** @var CreatedAccountAuthenticationRequest[] */
146 private $createdAccountAuthenticationRequests = [];
149 * Get the global AuthManager
150 * @return AuthManager
152 public static function singleton() {
153 if ( self
::$instance === null ) {
154 self
::$instance = new self(
155 \RequestContext
::getMain()->getRequest(),
156 MediaWikiServices
::getInstance()->getMainConfig()
159 return self
::$instance;
163 * @param WebRequest $request
164 * @param Config $config
166 public function __construct( WebRequest
$request, Config
$config ) {
167 $this->request
= $request;
168 $this->config
= $config;
169 $this->setLogger( \MediaWiki\Logger\LoggerFactory
::getInstance( 'authentication' ) );
173 * @param LoggerInterface $logger
175 public function setLogger( LoggerInterface
$logger ) {
176 $this->logger
= $logger;
182 public function getRequest() {
183 return $this->request
;
187 * Force certain PrimaryAuthenticationProviders
188 * @deprecated For backwards compatibility only
189 * @param PrimaryAuthenticationProvider[] $providers
192 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
193 $this->logger
->warning( "Overriding AuthManager primary authn because $why" );
195 if ( $this->primaryAuthenticationProviders
!== null ) {
196 $this->logger
->warning(
197 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
200 $this->allAuthenticationProviders
= array_diff_key(
201 $this->allAuthenticationProviders
,
202 $this->primaryAuthenticationProviders
204 $session = $this->request
->getSession();
205 $session->remove( 'AuthManager::authnState' );
206 $session->remove( 'AuthManager::accountCreationState' );
207 $session->remove( 'AuthManager::accountLinkState' );
208 $this->createdAccountAuthenticationRequests
= [];
211 $this->primaryAuthenticationProviders
= [];
212 foreach ( $providers as $provider ) {
213 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
214 throw new \
RuntimeException(
215 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
216 get_class( $provider )
219 $provider->setLogger( $this->logger
);
220 $provider->setManager( $this );
221 $provider->setConfig( $this->config
);
222 $id = $provider->getUniqueId();
223 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
224 throw new \
RuntimeException(
225 "Duplicate specifications for id $id (classes " .
226 get_class( $provider ) . ' and ' .
227 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
230 $this->allAuthenticationProviders
[$id] = $provider;
231 $this->primaryAuthenticationProviders
[$id] = $provider;
236 * This used to call a legacy AuthPlugin method, if necessary. Since that code has
237 * been removed, it now just returns the $return parameter.
239 * @codeCoverageIgnore
240 * @deprecated For backwards compatibility only, should be avoided in new code
241 * @param string $method AuthPlugin method to call
242 * @param array $params Parameters to pass
243 * @param mixed $return Return value if AuthPlugin wasn't called
244 * @return mixed Return value from the AuthPlugin method, or $return
246 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
247 wfDeprecated( __METHOD__
, '1.33' );
252 * @name Authentication
257 * Indicate whether user authentication is possible
259 * It may not be if the session is provided by something like OAuth
260 * for which each individual request includes authentication data.
264 public function canAuthenticateNow() {
265 return $this->request
->getSession()->canSetUser();
269 * Start an authentication flow
271 * In addition to the AuthenticationRequests returned by
272 * $this->getAuthenticationRequests(), a client might include a
273 * CreateFromLoginAuthenticationRequest from a previous login attempt to
276 * Instead of the AuthenticationRequests returned by
277 * $this->getAuthenticationRequests(), a client might pass a
278 * CreatedAccountAuthenticationRequest from an account creation that just
279 * succeeded to log in to the just-created account.
281 * @param AuthenticationRequest[] $reqs
282 * @param string $returnToUrl Url that REDIRECT responses should eventually
284 * @return AuthenticationResponse See self::continueAuthentication()
286 public function beginAuthentication( array $reqs, $returnToUrl ) {
287 $session = $this->request
->getSession();
288 if ( !$session->canSetUser() ) {
289 // Caller should have called canAuthenticateNow()
290 $session->remove( 'AuthManager::authnState' );
291 throw new \
LogicException( 'Authentication is not possible now' );
294 $guessUserName = null;
295 foreach ( $reqs as $req ) {
296 $req->returnToUrl
= $returnToUrl;
297 // @codeCoverageIgnoreStart
298 if ( $req->username
!== null && $req->username
!== '' ) {
299 if ( $guessUserName === null ) {
300 $guessUserName = $req->username
;
301 } elseif ( $guessUserName !== $req->username
) {
302 $guessUserName = null;
306 // @codeCoverageIgnoreEnd
309 // Check for special-case login of a just-created account
310 $req = AuthenticationRequest
::getRequestByClass(
311 $reqs, CreatedAccountAuthenticationRequest
::class
314 if ( !in_array( $req, $this->createdAccountAuthenticationRequests
, true ) ) {
315 throw new \
LogicException(
316 'CreatedAccountAuthenticationRequests are only valid on ' .
317 'the same AuthManager that created the account'
321 $user = User
::newFromName( $req->username
);
322 // @codeCoverageIgnoreStart
324 throw new \
UnexpectedValueException(
325 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
327 } elseif ( $user->getId() != $req->id
) {
328 throw new \
UnexpectedValueException(
329 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
332 // @codeCoverageIgnoreEnd
334 $this->logger
->info( 'Logging in {user} after account creation', [
335 'user' => $user->getName(),
337 $ret = AuthenticationResponse
::newPass( $user->getName() );
338 $this->setSessionDataForUser( $user );
339 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
340 $session->remove( 'AuthManager::authnState' );
341 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
345 $this->removeAuthenticationSessionData( null );
347 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
348 $status = $provider->testForAuthentication( $reqs );
349 if ( !$status->isGood() ) {
350 $this->logger
->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
351 $ret = AuthenticationResponse
::newFail(
352 Status
::wrap( $status )->getMessage()
354 $this->callMethodOnProviders( 7, 'postAuthentication',
355 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
357 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] );
364 'returnToUrl' => $returnToUrl,
365 'guessUserName' => $guessUserName,
367 'primaryResponse' => null,
370 'continueRequests' => [],
373 // Preserve state from a previous failed login
374 $req = AuthenticationRequest
::getRequestByClass(
375 $reqs, CreateFromLoginAuthenticationRequest
::class
378 $state['maybeLink'] = $req->maybeLink
;
381 $session = $this->request
->getSession();
382 $session->setSecret( 'AuthManager::authnState', $state );
385 return $this->continueAuthentication( $reqs );
389 * Continue an authentication flow
391 * Return values are interpreted as follows:
392 * - status FAIL: Authentication failed. If $response->createRequest is
393 * set, that may be passed to self::beginAuthentication() or to
394 * self::beginAccountCreation() to preserve state.
395 * - status REDIRECT: The client should be redirected to the contained URL,
396 * new AuthenticationRequests should be made (if any), then
397 * AuthManager::continueAuthentication() should be called.
398 * - status UI: The client should be presented with a user interface for
399 * the fields in the specified AuthenticationRequests, then new
400 * AuthenticationRequests should be made, then
401 * AuthManager::continueAuthentication() should be called.
402 * - status RESTART: The user logged in successfully with a third-party
403 * service, but the third-party credentials aren't attached to any local
404 * account. This could be treated as a UI or a FAIL.
405 * - status PASS: Authentication was successful.
407 * @param AuthenticationRequest[] $reqs
408 * @return AuthenticationResponse
410 public function continueAuthentication( array $reqs ) {
411 $session = $this->request
->getSession();
413 if ( !$session->canSetUser() ) {
414 // Caller should have called canAuthenticateNow()
415 // @codeCoverageIgnoreStart
416 throw new \
LogicException( 'Authentication is not possible now' );
417 // @codeCoverageIgnoreEnd
420 $state = $session->getSecret( 'AuthManager::authnState' );
421 if ( !is_array( $state ) ) {
422 return AuthenticationResponse
::newFail(
423 wfMessage( 'authmanager-authn-not-in-progress' )
426 $state['continueRequests'] = [];
428 $guessUserName = $state['guessUserName'];
430 foreach ( $reqs as $req ) {
431 $req->returnToUrl
= $state['returnToUrl'];
434 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
436 if ( $state['primary'] === null ) {
437 // We haven't picked a PrimaryAuthenticationProvider yet
438 // @codeCoverageIgnoreStart
439 $guessUserName = null;
440 foreach ( $reqs as $req ) {
441 if ( $req->username
!== null && $req->username
!== '' ) {
442 if ( $guessUserName === null ) {
443 $guessUserName = $req->username
;
444 } elseif ( $guessUserName !== $req->username
) {
445 $guessUserName = null;
450 $state['guessUserName'] = $guessUserName;
451 // @codeCoverageIgnoreEnd
452 $state['reqs'] = $reqs;
454 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
455 $res = $provider->beginPrimaryAuthentication( $reqs );
456 switch ( $res->status
) {
457 case AuthenticationResponse
::PASS
;
458 $state['primary'] = $id;
459 $state['primaryResponse'] = $res;
460 $this->logger
->debug( "Primary login with $id succeeded" );
462 case AuthenticationResponse
::FAIL
;
463 $this->logger
->debug( "Login failed in primary authentication by $id" );
464 if ( $res->createRequest ||
$state['maybeLink'] ) {
465 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
466 $res->createRequest
, $state['maybeLink']
469 $this->callMethodOnProviders( 7, 'postAuthentication',
470 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
472 $session->remove( 'AuthManager::authnState' );
473 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
475 case AuthenticationResponse
::ABSTAIN
;
478 case AuthenticationResponse
::REDIRECT
;
479 case AuthenticationResponse
::UI
;
480 $this->logger
->debug( "Primary login with $id returned $res->status" );
481 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
482 $state['primary'] = $id;
483 $state['continueRequests'] = $res->neededRequests
;
484 $session->setSecret( 'AuthManager::authnState', $state );
487 // @codeCoverageIgnoreStart
489 throw new \
DomainException(
490 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
492 // @codeCoverageIgnoreEnd
495 if ( $state['primary'] === null ) {
496 $this->logger
->debug( 'Login failed in primary authentication because no provider accepted' );
497 $ret = AuthenticationResponse
::newFail(
498 wfMessage( 'authmanager-authn-no-primary' )
500 $this->callMethodOnProviders( 7, 'postAuthentication',
501 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
503 $session->remove( 'AuthManager::authnState' );
506 } elseif ( $state['primaryResponse'] === null ) {
507 $provider = $this->getAuthenticationProvider( $state['primary'] );
508 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
509 // Configuration changed? Force them to start over.
510 // @codeCoverageIgnoreStart
511 $ret = AuthenticationResponse
::newFail(
512 wfMessage( 'authmanager-authn-not-in-progress' )
514 $this->callMethodOnProviders( 7, 'postAuthentication',
515 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
517 $session->remove( 'AuthManager::authnState' );
519 // @codeCoverageIgnoreEnd
521 $id = $provider->getUniqueId();
522 $res = $provider->continuePrimaryAuthentication( $reqs );
523 switch ( $res->status
) {
524 case AuthenticationResponse
::PASS
;
525 $state['primaryResponse'] = $res;
526 $this->logger
->debug( "Primary login with $id succeeded" );
528 case AuthenticationResponse
::FAIL
;
529 $this->logger
->debug( "Login failed in primary authentication by $id" );
530 if ( $res->createRequest ||
$state['maybeLink'] ) {
531 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
532 $res->createRequest
, $state['maybeLink']
535 $this->callMethodOnProviders( 7, 'postAuthentication',
536 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
538 $session->remove( 'AuthManager::authnState' );
539 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
541 case AuthenticationResponse
::REDIRECT
;
542 case AuthenticationResponse
::UI
;
543 $this->logger
->debug( "Primary login with $id returned $res->status" );
544 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
545 $state['continueRequests'] = $res->neededRequests
;
546 $session->setSecret( 'AuthManager::authnState', $state );
549 throw new \
DomainException(
550 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
555 $res = $state['primaryResponse'];
556 if ( $res->username
=== null ) {
557 $provider = $this->getAuthenticationProvider( $state['primary'] );
558 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
559 // Configuration changed? Force them to start over.
560 // @codeCoverageIgnoreStart
561 $ret = AuthenticationResponse
::newFail(
562 wfMessage( 'authmanager-authn-not-in-progress' )
564 $this->callMethodOnProviders( 7, 'postAuthentication',
565 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
567 $session->remove( 'AuthManager::authnState' );
569 // @codeCoverageIgnoreEnd
572 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
&&
574 // don't confuse the user with an incorrect message if linking is disabled
575 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider
::class )
577 $state['maybeLink'][$res->linkRequest
->getUniqueId()] = $res->linkRequest
;
578 $msg = 'authmanager-authn-no-local-user-link';
580 $msg = 'authmanager-authn-no-local-user';
582 $this->logger
->debug(
583 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
585 $ret = AuthenticationResponse
::newRestart( wfMessage( $msg ) );
586 $ret->neededRequests
= $this->getAuthenticationRequestsInternal(
589 $this->getPrimaryAuthenticationProviders() +
$this->getSecondaryAuthenticationProviders()
591 if ( $res->createRequest ||
$state['maybeLink'] ) {
592 $ret->createRequest
= new CreateFromLoginAuthenticationRequest(
593 $res->createRequest
, $state['maybeLink']
595 $ret->neededRequests
[] = $ret->createRequest
;
597 $this->fillRequests( $ret->neededRequests
, self
::ACTION_LOGIN
, null, true );
598 $session->setSecret( 'AuthManager::authnState', [
599 'reqs' => [], // Will be filled in later
601 'primaryResponse' => null,
603 'continueRequests' => $ret->neededRequests
,
608 // Step 2: Primary authentication succeeded, create the User object
609 // (and add the user locally if necessary)
611 $user = User
::newFromName( $res->username
, 'usable' );
613 $provider = $this->getAuthenticationProvider( $state['primary'] );
614 throw new \
DomainException(
615 get_class( $provider ) . " returned an invalid username: {$res->username}"
618 if ( $user->getId() === 0 ) {
619 // User doesn't exist locally. Create it.
620 $this->logger
->info( 'Auto-creating {user} on login', [
621 'user' => $user->getName(),
623 $status = $this->autoCreateUser( $user, $state['primary'], false );
624 if ( !$status->isGood() ) {
625 $ret = AuthenticationResponse
::newFail(
626 Status
::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
628 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
629 $session->remove( 'AuthManager::authnState' );
630 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
635 // Step 3: Iterate over all the secondary authentication providers.
637 $beginReqs = $state['reqs'];
639 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
640 if ( !isset( $state['secondary'][$id] ) ) {
641 // This provider isn't started yet, so we pass it the set
642 // of reqs from beginAuthentication instead of whatever
643 // might have been used by a previous provider in line.
644 $func = 'beginSecondaryAuthentication';
645 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
646 } elseif ( !$state['secondary'][$id] ) {
647 $func = 'continueSecondaryAuthentication';
648 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
652 switch ( $res->status
) {
653 case AuthenticationResponse
::PASS
;
654 $this->logger
->debug( "Secondary login with $id succeeded" );
656 case AuthenticationResponse
::ABSTAIN
;
657 $state['secondary'][$id] = true;
659 case AuthenticationResponse
::FAIL
;
660 $this->logger
->debug( "Login failed in secondary authentication by $id" );
661 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
662 $session->remove( 'AuthManager::authnState' );
663 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] );
665 case AuthenticationResponse
::REDIRECT
;
666 case AuthenticationResponse
::UI
;
667 $this->logger
->debug( "Secondary login with $id returned " . $res->status
);
668 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $user->getName() );
669 $state['secondary'][$id] = false;
670 $state['continueRequests'] = $res->neededRequests
;
671 $session->setSecret( 'AuthManager::authnState', $state );
674 // @codeCoverageIgnoreStart
676 throw new \
DomainException(
677 get_class( $provider ) . "::{$func}() returned $res->status"
679 // @codeCoverageIgnoreEnd
683 // Step 4: Authentication complete! Set the user in the session and
686 $this->logger
->info( 'Login for {user} succeeded from {clientip}', [
687 'user' => $user->getName(),
688 'clientip' => $this->request
->getIP(),
690 /** @var RememberMeAuthenticationRequest $req */
691 $req = AuthenticationRequest
::getRequestByClass(
692 $beginReqs, RememberMeAuthenticationRequest
::class
694 $this->setSessionDataForUser( $user, $req && $req->rememberMe
);
695 $ret = AuthenticationResponse
::newPass( $user->getName() );
696 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
697 $session->remove( 'AuthManager::authnState' );
698 $this->removeAuthenticationSessionData( null );
699 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
701 } catch ( \Exception
$ex ) {
702 $session->remove( 'AuthManager::authnState' );
708 * Whether security-sensitive operations should proceed.
710 * A "security-sensitive operation" is something like a password or email
711 * change, that would normally have a "reenter your password to confirm"
712 * box if we only supported password-based authentication.
714 * @param string $operation Operation being checked. This should be a
715 * message-key-like string such as 'change-password' or 'change-email'.
716 * @return string One of the SEC_* constants.
718 public function securitySensitiveOperationStatus( $operation ) {
719 $status = self
::SEC_OK
;
721 $this->logger
->debug( __METHOD__
. ": Checking $operation" );
723 $session = $this->request
->getSession();
724 $aId = $session->getUser()->getId();
726 // User isn't authenticated. DWIM?
727 $status = $this->canAuthenticateNow() ? self
::SEC_REAUTH
: self
::SEC_FAIL
;
728 $this->logger
->info( __METHOD__
. ": Not logged in! $operation is $status" );
732 if ( $session->canSetUser() ) {
733 $id = $session->get( 'AuthManager:lastAuthId' );
734 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
735 if ( $id !== $aId ||
$last === null ) {
736 $timeSinceLogin = PHP_INT_MAX
; // Forever ago
738 $timeSinceLogin = max( 0, time() - $last );
741 $thresholds = $this->config
->get( 'ReauthenticateTime' );
742 if ( isset( $thresholds[$operation] ) ) {
743 $threshold = $thresholds[$operation];
744 } elseif ( isset( $thresholds['default'] ) ) {
745 $threshold = $thresholds['default'];
747 throw new \
UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
750 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
751 $status = self
::SEC_REAUTH
;
754 $timeSinceLogin = -1;
756 $pass = $this->config
->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
757 if ( isset( $pass[$operation] ) ) {
758 $status = $pass[$operation] ? self
::SEC_OK
: self
::SEC_FAIL
;
759 } elseif ( isset( $pass['default'] ) ) {
760 $status = $pass['default'] ? self
::SEC_OK
: self
::SEC_FAIL
;
762 throw new \
UnexpectedValueException(
763 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
768 \Hooks
::run( 'SecuritySensitiveOperationStatus', [
769 &$status, $operation, $session, $timeSinceLogin
772 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
773 if ( !$this->canAuthenticateNow() && $status === self
::SEC_REAUTH
) {
774 $status = self
::SEC_FAIL
;
777 $this->logger
->info( __METHOD__
. ": $operation is $status for '{user}'",
779 'user' => $session->getUser()->getName(),
780 'clientip' => $this->getRequest()->getIP(),
788 * Determine whether a username can authenticate
790 * This is mainly for internal purposes and only takes authentication data into account,
791 * not things like blocks that can change without the authentication system being aware.
793 * @param string $username MediaWiki username
796 public function userCanAuthenticate( $username ) {
797 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
798 if ( $provider->testUserCanAuthenticate( $username ) ) {
806 * Provide normalized versions of the username for security checks
808 * Since different providers can normalize the input in different ways,
809 * this returns an array of all the different ways the name might be
810 * normalized for authentication.
812 * The returned strings should not be revealed to the user, as that might
813 * leak private information (e.g. an email address might be normalized to a
816 * @param string $username
819 public function normalizeUsername( $username ) {
821 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
822 $normalized = $provider->providerNormalizeUsername( $username );
823 if ( $normalized !== null ) {
824 $ret[$normalized] = true;
827 return array_keys( $ret );
833 * @name Authentication data changing
838 * Revoke any authentication credentials for a user
840 * After this, the user should no longer be able to log in.
842 * @param string $username
844 public function revokeAccessForUser( $username ) {
845 $this->logger
->info( 'Revoking access for {user}', [
848 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
852 * Validate a change of authentication data (e.g. passwords)
853 * @param AuthenticationRequest $req
854 * @param bool $checkData If false, $req hasn't been loaded from the
855 * submission so checks on user-submitted fields should be skipped. $req->username is
856 * considered user-submitted for this purpose, even if it cannot be changed via
857 * $req->loadFromSubmission.
860 public function allowsAuthenticationDataChange( AuthenticationRequest
$req, $checkData = true ) {
862 $providers = $this->getPrimaryAuthenticationProviders() +
863 $this->getSecondaryAuthenticationProviders();
864 foreach ( $providers as $provider ) {
865 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
866 if ( !$status->isGood() ) {
867 return Status
::wrap( $status );
869 $any = $any ||
$status->value
!== 'ignored';
872 $status = Status
::newGood( 'ignored' );
873 $status->warning( 'authmanager-change-not-supported' );
876 return Status
::newGood();
880 * Change authentication data (e.g. passwords)
882 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
883 * result in a successful login in the future.
885 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
886 * no longer result in a successful login.
888 * This method should only be called if allowsAuthenticationDataChange( $req, true )
891 * @param AuthenticationRequest $req
892 * @param bool $isAddition Set true if this represents an addition of
893 * credentials rather than a change. The main difference is that additions
894 * should not invalidate BotPasswords. If you're not sure, leave it false.
896 public function changeAuthenticationData( AuthenticationRequest
$req, $isAddition = false ) {
897 $this->logger
->info( 'Changing authentication data for {user} class {what}', [
898 'user' => is_string( $req->username
) ?
$req->username
: '<no name>',
899 'what' => get_class( $req ),
902 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
904 // When the main account's authentication data is changed, invalidate
905 // all BotPasswords too.
906 if ( !$isAddition ) {
907 \BotPassword
::invalidateAllPasswordsForUser( $req->username
);
914 * @name Account creation
919 * Determine whether accounts can be created
922 public function canCreateAccounts() {
923 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
924 switch ( $provider->accountCreationType() ) {
925 case PrimaryAuthenticationProvider
::TYPE_CREATE
:
926 case PrimaryAuthenticationProvider
::TYPE_LINK
:
934 * Determine whether a particular account can be created
935 * @param string $username MediaWiki username
936 * @param array $options
937 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
938 * - creating: (bool) For internal use only. Never specify this.
941 public function canCreateAccount( $username, $options = [] ) {
943 if ( is_int( $options ) ) {
944 $options = [ 'flags' => $options ];
947 'flags' => User
::READ_NORMAL
,
950 $flags = $options['flags'];
952 if ( !$this->canCreateAccounts() ) {
953 return Status
::newFatal( 'authmanager-create-disabled' );
956 if ( $this->userExists( $username, $flags ) ) {
957 return Status
::newFatal( 'userexists' );
960 $user = User
::newFromName( $username, 'creatable' );
961 if ( !is_object( $user ) ) {
962 return Status
::newFatal( 'noname' );
964 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
965 if ( $user->getId() !== 0 ) {
966 return Status
::newFatal( 'userexists' );
970 // Denied by providers?
971 $providers = $this->getPreAuthenticationProviders() +
972 $this->getPrimaryAuthenticationProviders() +
973 $this->getSecondaryAuthenticationProviders();
974 foreach ( $providers as $provider ) {
975 $status = $provider->testUserForCreation( $user, false, $options );
976 if ( !$status->isGood() ) {
977 return Status
::wrap( $status );
981 return Status
::newGood();
985 * Basic permissions checks on whether a user can create accounts
986 * @param User $creator User doing the account creation
989 public function checkAccountCreatePermissions( User
$creator ) {
990 // Wiki is read-only?
991 if ( wfReadOnly() ) {
992 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
995 // This is awful, this permission check really shouldn't go through Title.
996 $permErrors = \SpecialPage
::getTitleFor( 'CreateAccount' )
997 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
999 $status = Status
::newGood();
1000 foreach ( $permErrors as $args ) {
1001 $status->fatal( ...$args );
1006 $block = $creator->isBlockedFromCreateAccount();
1009 $block->getTarget(),
1010 $block->getReason() ?
: wfMessage( 'blockednoreason' )->text(),
1014 if ( $block->getType() === DatabaseBlock
::TYPE_RANGE
) {
1015 $errorMessage = 'cantcreateaccount-range-text';
1016 $errorParams[] = $this->getRequest()->getIP();
1018 $errorMessage = 'cantcreateaccount-text';
1021 return Status
::newFatal( wfMessage( $errorMessage, $errorParams ) );
1024 $ip = $this->getRequest()->getIP();
1026 MediaWikiServices
::getInstance()->getBlockManager()
1027 ->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ )
1029 return Status
::newFatal( 'sorbs_create_account_reason' );
1032 return Status
::newGood();
1036 * Start an account creation flow
1038 * In addition to the AuthenticationRequests returned by
1039 * $this->getAuthenticationRequests(), a client might include a
1040 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1042 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1044 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1045 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1046 * username set, that username must be used for all other requests.
1048 * @param User $creator User doing the account creation
1049 * @param AuthenticationRequest[] $reqs
1050 * @param string $returnToUrl Url that REDIRECT responses should eventually
1052 * @return AuthenticationResponse
1054 public function beginAccountCreation( User
$creator, array $reqs, $returnToUrl ) {
1055 $session = $this->request
->getSession();
1056 if ( !$this->canCreateAccounts() ) {
1057 // Caller should have called canCreateAccounts()
1058 $session->remove( 'AuthManager::accountCreationState' );
1059 throw new \
LogicException( 'Account creation is not possible' );
1063 $username = AuthenticationRequest
::getUsernameFromRequests( $reqs );
1064 } catch ( \UnexpectedValueException
$ex ) {
1067 if ( $username === null ) {
1068 $this->logger
->debug( __METHOD__
. ': No username provided' );
1069 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1072 // Permissions check
1073 $status = $this->checkAccountCreatePermissions( $creator );
1074 if ( !$status->isGood() ) {
1075 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1076 'user' => $username,
1077 'creator' => $creator->getName(),
1078 'reason' => $status->getWikiText( null, null, 'en' )
1080 return AuthenticationResponse
::newFail( $status->getMessage() );
1083 $status = $this->canCreateAccount(
1084 $username, [ 'flags' => User
::READ_LOCKING
, 'creating' => true ]
1086 if ( !$status->isGood() ) {
1087 $this->logger
->debug( __METHOD__
. ': {user} cannot be created: {reason}', [
1088 'user' => $username,
1089 'creator' => $creator->getName(),
1090 'reason' => $status->getWikiText( null, null, 'en' )
1092 return AuthenticationResponse
::newFail( $status->getMessage() );
1095 $user = User
::newFromName( $username, 'creatable' );
1096 foreach ( $reqs as $req ) {
1097 $req->username
= $username;
1098 $req->returnToUrl
= $returnToUrl;
1099 if ( $req instanceof UserDataAuthenticationRequest
) {
1100 $status = $req->populateUser( $user );
1101 if ( !$status->isGood() ) {
1102 $status = Status
::wrap( $status );
1103 $session->remove( 'AuthManager::accountCreationState' );
1104 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1105 'user' => $user->getName(),
1106 'creator' => $creator->getName(),
1107 'reason' => $status->getWikiText( null, null, 'en' ),
1109 return AuthenticationResponse
::newFail( $status->getMessage() );
1114 $this->removeAuthenticationSessionData( null );
1117 'username' => $username,
1119 'creatorid' => $creator->getId(),
1120 'creatorname' => $creator->getName(),
1122 'returnToUrl' => $returnToUrl,
1124 'primaryResponse' => null,
1126 'continueRequests' => [],
1128 'ranPreTests' => false,
1131 // Special case: converting a login to an account creation
1132 $req = AuthenticationRequest
::getRequestByClass(
1133 $reqs, CreateFromLoginAuthenticationRequest
::class
1136 $state['maybeLink'] = $req->maybeLink
;
1138 if ( $req->createRequest
) {
1139 $reqs[] = $req->createRequest
;
1140 $state['reqs'][] = $req->createRequest
;
1144 $session->setSecret( 'AuthManager::accountCreationState', $state );
1145 $session->persist();
1147 return $this->continueAccountCreation( $reqs );
1151 * Continue an account creation flow
1152 * @param AuthenticationRequest[] $reqs
1153 * @return AuthenticationResponse
1155 public function continueAccountCreation( array $reqs ) {
1156 $session = $this->request
->getSession();
1158 if ( !$this->canCreateAccounts() ) {
1159 // Caller should have called canCreateAccounts()
1160 $session->remove( 'AuthManager::accountCreationState' );
1161 throw new \
LogicException( 'Account creation is not possible' );
1164 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1165 if ( !is_array( $state ) ) {
1166 return AuthenticationResponse
::newFail(
1167 wfMessage( 'authmanager-create-not-in-progress' )
1170 $state['continueRequests'] = [];
1172 // Step 0: Prepare and validate the input
1174 $user = User
::newFromName( $state['username'], 'creatable' );
1175 if ( !is_object( $user ) ) {
1176 $session->remove( 'AuthManager::accountCreationState' );
1177 $this->logger
->debug( __METHOD__
. ': Invalid username', [
1178 'user' => $state['username'],
1180 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1183 if ( $state['creatorid'] ) {
1184 $creator = User
::newFromId( $state['creatorid'] );
1186 $creator = new User
;
1187 $creator->setName( $state['creatorname'] );
1190 // Avoid account creation races on double submissions
1191 $cache = \ObjectCache
::getLocalClusterInstance();
1192 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1194 // Don't clear AuthManager::accountCreationState for this code
1195 // path because the process that won the race owns it.
1196 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1197 'user' => $user->getName(),
1198 'creator' => $creator->getName(),
1200 return AuthenticationResponse
::newFail( wfMessage( 'usernameinprogress' ) );
1203 // Permissions check
1204 $status = $this->checkAccountCreatePermissions( $creator );
1205 if ( !$status->isGood() ) {
1206 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1207 'user' => $user->getName(),
1208 'creator' => $creator->getName(),
1209 'reason' => $status->getWikiText( null, null, 'en' )
1211 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1212 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1213 $session->remove( 'AuthManager::accountCreationState' );
1217 // Load from master for existence check
1218 $user->load( User
::READ_LOCKING
);
1220 if ( $state['userid'] === 0 ) {
1221 if ( $user->getId() !== 0 ) {
1222 $this->logger
->debug( __METHOD__
. ': User exists locally', [
1223 'user' => $user->getName(),
1224 'creator' => $creator->getName(),
1226 $ret = AuthenticationResponse
::newFail( wfMessage( 'userexists' ) );
1227 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1228 $session->remove( 'AuthManager::accountCreationState' );
1232 if ( $user->getId() === 0 ) {
1233 $this->logger
->debug( __METHOD__
. ': User does not exist locally when it should', [
1234 'user' => $user->getName(),
1235 'creator' => $creator->getName(),
1236 'expected_id' => $state['userid'],
1238 throw new \
UnexpectedValueException(
1239 "User \"{$state['username']}\" should exist now, but doesn't!"
1242 if ( $user->getId() !== $state['userid'] ) {
1243 $this->logger
->debug( __METHOD__
. ': User ID/name mismatch', [
1244 'user' => $user->getName(),
1245 'creator' => $creator->getName(),
1246 'expected_id' => $state['userid'],
1247 'actual_id' => $user->getId(),
1249 throw new \
UnexpectedValueException(
1250 "User \"{$state['username']}\" exists, but " .
1251 "ID {$user->getId()} !== {$state['userid']}!"
1255 foreach ( $state['reqs'] as $req ) {
1256 if ( $req instanceof UserDataAuthenticationRequest
) {
1257 $status = $req->populateUser( $user );
1258 if ( !$status->isGood() ) {
1259 // This should never happen...
1260 $status = Status
::wrap( $status );
1261 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1262 'user' => $user->getName(),
1263 'creator' => $creator->getName(),
1264 'reason' => $status->getWikiText( null, null, 'en' ),
1266 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1267 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1268 $session->remove( 'AuthManager::accountCreationState' );
1274 foreach ( $reqs as $req ) {
1275 $req->returnToUrl
= $state['returnToUrl'];
1276 $req->username
= $state['username'];
1279 // Run pre-creation tests, if we haven't already
1280 if ( !$state['ranPreTests'] ) {
1281 $providers = $this->getPreAuthenticationProviders() +
1282 $this->getPrimaryAuthenticationProviders() +
1283 $this->getSecondaryAuthenticationProviders();
1284 foreach ( $providers as $id => $provider ) {
1285 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1286 if ( !$status->isGood() ) {
1287 $this->logger
->debug( __METHOD__
. ": Fail in pre-authentication by $id", [
1288 'user' => $user->getName(),
1289 'creator' => $creator->getName(),
1291 $ret = AuthenticationResponse
::newFail(
1292 Status
::wrap( $status )->getMessage()
1294 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1295 $session->remove( 'AuthManager::accountCreationState' );
1300 $state['ranPreTests'] = true;
1303 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1305 if ( $state['primary'] === null ) {
1306 // We haven't picked a PrimaryAuthenticationProvider yet
1307 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1308 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_NONE
) {
1311 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1312 switch ( $res->status
) {
1313 case AuthenticationResponse
::PASS
;
1314 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1315 'user' => $user->getName(),
1316 'creator' => $creator->getName(),
1318 $state['primary'] = $id;
1319 $state['primaryResponse'] = $res;
1321 case AuthenticationResponse
::FAIL
;
1322 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1323 'user' => $user->getName(),
1324 'creator' => $creator->getName(),
1326 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1327 $session->remove( 'AuthManager::accountCreationState' );
1329 case AuthenticationResponse
::ABSTAIN
;
1332 case AuthenticationResponse
::REDIRECT
;
1333 case AuthenticationResponse
::UI
;
1334 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1335 'user' => $user->getName(),
1336 'creator' => $creator->getName(),
1338 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1339 $state['primary'] = $id;
1340 $state['continueRequests'] = $res->neededRequests
;
1341 $session->setSecret( 'AuthManager::accountCreationState', $state );
1344 // @codeCoverageIgnoreStart
1346 throw new \
DomainException(
1347 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1349 // @codeCoverageIgnoreEnd
1352 if ( $state['primary'] === null ) {
1353 $this->logger
->debug( __METHOD__
. ': Primary creation failed because no provider accepted', [
1354 'user' => $user->getName(),
1355 'creator' => $creator->getName(),
1357 $ret = AuthenticationResponse
::newFail(
1358 wfMessage( 'authmanager-create-no-primary' )
1360 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1361 $session->remove( 'AuthManager::accountCreationState' );
1364 } elseif ( $state['primaryResponse'] === null ) {
1365 $provider = $this->getAuthenticationProvider( $state['primary'] );
1366 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1367 // Configuration changed? Force them to start over.
1368 // @codeCoverageIgnoreStart
1369 $ret = AuthenticationResponse
::newFail(
1370 wfMessage( 'authmanager-create-not-in-progress' )
1372 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1373 $session->remove( 'AuthManager::accountCreationState' );
1375 // @codeCoverageIgnoreEnd
1377 $id = $provider->getUniqueId();
1378 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1379 switch ( $res->status
) {
1380 case AuthenticationResponse
::PASS
;
1381 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1382 'user' => $user->getName(),
1383 'creator' => $creator->getName(),
1385 $state['primaryResponse'] = $res;
1387 case AuthenticationResponse
::FAIL
;
1388 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1389 'user' => $user->getName(),
1390 'creator' => $creator->getName(),
1392 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1393 $session->remove( 'AuthManager::accountCreationState' );
1395 case AuthenticationResponse
::REDIRECT
;
1396 case AuthenticationResponse
::UI
;
1397 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1398 'user' => $user->getName(),
1399 'creator' => $creator->getName(),
1401 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1402 $state['continueRequests'] = $res->neededRequests
;
1403 $session->setSecret( 'AuthManager::accountCreationState', $state );
1406 throw new \
DomainException(
1407 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1412 // Step 2: Primary authentication succeeded, create the User object
1413 // and add the user locally.
1415 if ( $state['userid'] === 0 ) {
1416 $this->logger
->info( 'Creating user {user} during account creation', [
1417 'user' => $user->getName(),
1418 'creator' => $creator->getName(),
1420 $status = $user->addToDatabase();
1421 if ( !$status->isOK() ) {
1422 // @codeCoverageIgnoreStart
1423 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1424 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1425 $session->remove( 'AuthManager::accountCreationState' );
1427 // @codeCoverageIgnoreEnd
1429 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1430 \Hooks
::runWithoutAbort( 'LocalUserCreated', [ $user, false ] );
1431 $user->saveSettings();
1432 $state['userid'] = $user->getId();
1434 // Update user count
1435 \DeferredUpdates
::addUpdate( \SiteStatsUpdate
::factory( [ 'users' => 1 ] ) );
1437 // Watch user's userpage and talk page
1438 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1440 // Inform the provider
1441 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1444 if ( $this->config
->get( 'NewUserLog' ) ) {
1445 $isAnon = $creator->isAnon();
1446 $logEntry = new \
ManualLogEntry(
1448 $logSubtype ?
: ( $isAnon ?
'create' : 'create2' )
1450 $logEntry->setPerformer( $isAnon ?
$user : $creator );
1451 $logEntry->setTarget( $user->getUserPage() );
1452 /** @var CreationReasonAuthenticationRequest $req */
1453 $req = AuthenticationRequest
::getRequestByClass(
1454 $state['reqs'], CreationReasonAuthenticationRequest
::class
1456 $logEntry->setComment( $req ?
$req->reason
: '' );
1457 $logEntry->setParameters( [
1458 '4::userid' => $user->getId(),
1460 $logid = $logEntry->insert();
1461 $logEntry->publish( $logid );
1465 // Step 3: Iterate over all the secondary authentication providers.
1467 $beginReqs = $state['reqs'];
1469 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1470 if ( !isset( $state['secondary'][$id] ) ) {
1471 // This provider isn't started yet, so we pass it the set
1472 // of reqs from beginAuthentication instead of whatever
1473 // might have been used by a previous provider in line.
1474 $func = 'beginSecondaryAccountCreation';
1475 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1476 } elseif ( !$state['secondary'][$id] ) {
1477 $func = 'continueSecondaryAccountCreation';
1478 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1482 switch ( $res->status
) {
1483 case AuthenticationResponse
::PASS
;
1484 $this->logger
->debug( __METHOD__
. ": Secondary creation passed by $id", [
1485 'user' => $user->getName(),
1486 'creator' => $creator->getName(),
1489 case AuthenticationResponse
::ABSTAIN
;
1490 $state['secondary'][$id] = true;
1492 case AuthenticationResponse
::REDIRECT
;
1493 case AuthenticationResponse
::UI
;
1494 $this->logger
->debug( __METHOD__
. ": Secondary creation $res->status by $id", [
1495 'user' => $user->getName(),
1496 'creator' => $creator->getName(),
1498 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1499 $state['secondary'][$id] = false;
1500 $state['continueRequests'] = $res->neededRequests
;
1501 $session->setSecret( 'AuthManager::accountCreationState', $state );
1503 case AuthenticationResponse
::FAIL
;
1504 throw new \
DomainException(
1505 get_class( $provider ) . "::{$func}() returned $res->status." .
1506 ' Secondary providers are not allowed to fail account creation, that' .
1507 ' should have been done via testForAccountCreation().'
1509 // @codeCoverageIgnoreStart
1511 throw new \
DomainException(
1512 get_class( $provider ) . "::{$func}() returned $res->status"
1514 // @codeCoverageIgnoreEnd
1518 $id = $user->getId();
1519 $name = $user->getName();
1520 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1521 $ret = AuthenticationResponse
::newPass( $name );
1522 $ret->loginRequest
= $req;
1523 $this->createdAccountAuthenticationRequests
[] = $req;
1525 $this->logger
->info( __METHOD__
. ': Account creation succeeded for {user}', [
1526 'user' => $user->getName(),
1527 'creator' => $creator->getName(),
1530 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1531 $session->remove( 'AuthManager::accountCreationState' );
1532 $this->removeAuthenticationSessionData( null );
1534 } catch ( \Exception
$ex ) {
1535 $session->remove( 'AuthManager::accountCreationState' );
1541 * Auto-create an account, and log into that account
1543 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1544 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1545 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1546 * the username of a non-existing user from provideSessionInfo(). Calling this method
1547 * explicitly (e.g. from a maintenance script) is also fine.
1549 * @param User $user User to auto-create
1550 * @param string $source What caused the auto-creation? This must be one of:
1551 * - the ID of a PrimaryAuthenticationProvider,
1552 * - the constant self::AUTOCREATE_SOURCE_SESSION, or
1553 * - the constant AUTOCREATE_SOURCE_MAINT.
1554 * @param bool $login Whether to also log the user in
1555 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1557 public function autoCreateUser( User
$user, $source, $login = true ) {
1558 if ( $source !== self
::AUTOCREATE_SOURCE_SESSION
&&
1559 $source !== self
::AUTOCREATE_SOURCE_MAINT
&&
1560 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1562 throw new \
InvalidArgumentException( "Unknown auto-creation source: $source" );
1565 $username = $user->getName();
1567 // Try the local user from the replica DB
1568 $localId = User
::idFromName( $username );
1569 $flags = User
::READ_NORMAL
;
1571 // Fetch the user ID from the master, so that we don't try to create the user
1572 // when they already exist, due to replication lag
1573 // @codeCoverageIgnoreStart
1576 MediaWikiServices
::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1578 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1579 $flags = User
::READ_LATEST
;
1581 // @codeCoverageIgnoreEnd
1584 $this->logger
->debug( __METHOD__
. ': {username} already exists locally', [
1585 'username' => $username,
1587 $user->setId( $localId );
1588 $user->loadFromId( $flags );
1590 $this->setSessionDataForUser( $user );
1592 $status = Status
::newGood();
1593 $status->warning( 'userexists' );
1597 // Wiki is read-only?
1598 if ( wfReadOnly() ) {
1599 $this->logger
->debug( __METHOD__
. ': denied by wfReadOnly(): {reason}', [
1600 'username' => $username,
1601 'reason' => wfReadOnlyReason(),
1604 $user->loadFromId();
1605 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1608 // Check the session, if we tried to create this user already there's
1609 // no point in retrying.
1610 $session = $this->request
->getSession();
1611 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1612 $this->logger
->debug( __METHOD__
. ': blacklisted in session {sessionid}', [
1613 'username' => $username,
1614 'sessionid' => $session->getId(),
1617 $user->loadFromId();
1618 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1619 if ( $reason instanceof StatusValue
) {
1620 return Status
::wrap( $reason );
1622 return Status
::newFatal( $reason );
1626 // Is the username creatable?
1627 if ( !User
::isCreatableName( $username ) ) {
1628 $this->logger
->debug( __METHOD__
. ': name "{username}" is not creatable', [
1629 'username' => $username,
1631 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1633 $user->loadFromId();
1634 return Status
::newFatal( 'noname' );
1637 // Is the IP user able to create accounts?
1639 if ( $source !== self
::AUTOCREATE_SOURCE_MAINT
&&
1640 !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1642 $this->logger
->debug( __METHOD__
. ': IP lacks the ability to create or autocreate accounts', [
1643 'username' => $username,
1644 'ip' => $anon->getName(),
1646 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1647 $session->persist();
1649 $user->loadFromId();
1650 return Status
::newFatal( 'authmanager-autocreate-noperm' );
1653 // Avoid account creation races on double submissions
1654 $cache = \ObjectCache
::getLocalClusterInstance();
1655 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1657 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1658 'user' => $username,
1661 $user->loadFromId();
1662 return Status
::newFatal( 'usernameinprogress' );
1665 // Denied by providers?
1667 'flags' => User
::READ_LATEST
,
1670 $providers = $this->getPreAuthenticationProviders() +
1671 $this->getPrimaryAuthenticationProviders() +
1672 $this->getSecondaryAuthenticationProviders();
1673 foreach ( $providers as $provider ) {
1674 $status = $provider->testUserForCreation( $user, $source, $options );
1675 if ( !$status->isGood() ) {
1676 $ret = Status
::wrap( $status );
1677 $this->logger
->debug( __METHOD__
. ': Provider denied creation of {username}: {reason}', [
1678 'username' => $username,
1679 'reason' => $ret->getWikiText( null, null, 'en' ),
1681 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1683 $user->loadFromId();
1688 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1689 if ( $cache->get( $backoffKey ) ) {
1690 $this->logger
->debug( __METHOD__
. ': {username} denied by prior creation attempt failures', [
1691 'username' => $username,
1694 $user->loadFromId();
1695 return Status
::newFatal( 'authmanager-autocreate-exception' );
1698 // Checks passed, create the user...
1699 $from = $_SERVER['REQUEST_URI'] ??
'CLI';
1700 $this->logger
->info( __METHOD__
. ': creating new user ({username}) - from: {from}', [
1701 'username' => $username,
1705 // Ignore warnings about master connections/writes...hard to avoid here
1706 $trxProfiler = \Profiler
::instance()->getTransactionProfiler();
1707 $old = $trxProfiler->setSilenced( true );
1709 $status = $user->addToDatabase();
1710 if ( !$status->isOK() ) {
1711 // Double-check for a race condition (T70012). We make use of the fact that when
1712 // addToDatabase fails due to the user already existing, the user object gets loaded.
1713 if ( $user->getId() ) {
1714 $this->logger
->info( __METHOD__
. ': {username} already exists locally (race)', [
1715 'username' => $username,
1718 $this->setSessionDataForUser( $user );
1720 $status = Status
::newGood();
1721 $status->warning( 'userexists' );
1723 $this->logger
->error( __METHOD__
. ': {username} failed with message {msg}', [
1724 'username' => $username,
1725 'msg' => $status->getWikiText( null, null, 'en' )
1728 $user->loadFromId();
1732 } catch ( \Exception
$ex ) {
1733 $trxProfiler->setSilenced( $old );
1734 $this->logger
->error( __METHOD__
. ': {username} failed with exception {exception}', [
1735 'username' => $username,
1738 // Do not keep throwing errors for a while
1739 $cache->set( $backoffKey, 1, 600 );
1740 // Bubble up error; which should normally trigger DB rollbacks
1744 $this->setDefaultUserOptions( $user, false );
1746 // Inform the providers
1747 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1749 \Hooks
::run( 'LocalUserCreated', [ $user, true ] );
1750 $user->saveSettings();
1752 // Update user count
1753 \DeferredUpdates
::addUpdate( \SiteStatsUpdate
::factory( [ 'users' => 1 ] ) );
1754 // Watch user's userpage and talk page
1755 \DeferredUpdates
::addCallableUpdate( function () use ( $user ) {
1756 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1760 if ( $this->config
->get( 'NewUserLog' ) ) {
1761 $logEntry = new \
ManualLogEntry( 'newusers', 'autocreate' );
1762 $logEntry->setPerformer( $user );
1763 $logEntry->setTarget( $user->getUserPage() );
1764 $logEntry->setComment( '' );
1765 $logEntry->setParameters( [
1766 '4::userid' => $user->getId(),
1768 $logEntry->insert();
1771 $trxProfiler->setSilenced( $old );
1774 $this->setSessionDataForUser( $user );
1777 return Status
::newGood();
1783 * @name Account linking
1788 * Determine whether accounts can be linked
1791 public function canLinkAccounts() {
1792 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1793 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
) {
1801 * Start an account linking flow
1803 * @param User $user User being linked
1804 * @param AuthenticationRequest[] $reqs
1805 * @param string $returnToUrl Url that REDIRECT responses should eventually
1807 * @return AuthenticationResponse
1809 public function beginAccountLink( User
$user, array $reqs, $returnToUrl ) {
1810 $session = $this->request
->getSession();
1811 $session->remove( 'AuthManager::accountLinkState' );
1813 if ( !$this->canLinkAccounts() ) {
1814 // Caller should have called canLinkAccounts()
1815 throw new \
LogicException( 'Account linking is not possible' );
1818 if ( $user->getId() === 0 ) {
1819 if ( !User
::isUsableName( $user->getName() ) ) {
1820 $msg = wfMessage( 'noname' );
1822 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1824 return AuthenticationResponse
::newFail( $msg );
1826 foreach ( $reqs as $req ) {
1827 $req->username
= $user->getName();
1828 $req->returnToUrl
= $returnToUrl;
1831 $this->removeAuthenticationSessionData( null );
1833 $providers = $this->getPreAuthenticationProviders();
1834 foreach ( $providers as $id => $provider ) {
1835 $status = $provider->testForAccountLink( $user );
1836 if ( !$status->isGood() ) {
1837 $this->logger
->debug( __METHOD__
. ": Account linking pre-check failed by $id", [
1838 'user' => $user->getName(),
1840 $ret = AuthenticationResponse
::newFail(
1841 Status
::wrap( $status )->getMessage()
1843 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1849 'username' => $user->getName(),
1850 'userid' => $user->getId(),
1851 'returnToUrl' => $returnToUrl,
1853 'continueRequests' => [],
1856 $providers = $this->getPrimaryAuthenticationProviders();
1857 foreach ( $providers as $id => $provider ) {
1858 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider
::TYPE_LINK
) {
1862 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1863 switch ( $res->status
) {
1864 case AuthenticationResponse
::PASS
;
1865 $this->logger
->info( "Account linked to {user} by $id", [
1866 'user' => $user->getName(),
1868 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1871 case AuthenticationResponse
::FAIL
;
1872 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1873 'user' => $user->getName(),
1875 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1878 case AuthenticationResponse
::ABSTAIN
;
1882 case AuthenticationResponse
::REDIRECT
;
1883 case AuthenticationResponse
::UI
;
1884 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1885 'user' => $user->getName(),
1887 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1888 $state['primary'] = $id;
1889 $state['continueRequests'] = $res->neededRequests
;
1890 $session->setSecret( 'AuthManager::accountLinkState', $state );
1891 $session->persist();
1894 // @codeCoverageIgnoreStart
1896 throw new \
DomainException(
1897 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1899 // @codeCoverageIgnoreEnd
1903 $this->logger
->debug( __METHOD__
. ': Account linking failed because no provider accepted', [
1904 'user' => $user->getName(),
1906 $ret = AuthenticationResponse
::newFail(
1907 wfMessage( 'authmanager-link-no-primary' )
1909 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1914 * Continue an account linking flow
1915 * @param AuthenticationRequest[] $reqs
1916 * @return AuthenticationResponse
1918 public function continueAccountLink( array $reqs ) {
1919 $session = $this->request
->getSession();
1921 if ( !$this->canLinkAccounts() ) {
1922 // Caller should have called canLinkAccounts()
1923 $session->remove( 'AuthManager::accountLinkState' );
1924 throw new \
LogicException( 'Account linking is not possible' );
1927 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1928 if ( !is_array( $state ) ) {
1929 return AuthenticationResponse
::newFail(
1930 wfMessage( 'authmanager-link-not-in-progress' )
1933 $state['continueRequests'] = [];
1935 // Step 0: Prepare and validate the input
1937 $user = User
::newFromName( $state['username'], 'usable' );
1938 if ( !is_object( $user ) ) {
1939 $session->remove( 'AuthManager::accountLinkState' );
1940 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1942 if ( $user->getId() !== $state['userid'] ) {
1943 throw new \
UnexpectedValueException(
1944 "User \"{$state['username']}\" is valid, but " .
1945 "ID {$user->getId()} !== {$state['userid']}!"
1949 foreach ( $reqs as $req ) {
1950 $req->username
= $state['username'];
1951 $req->returnToUrl
= $state['returnToUrl'];
1954 // Step 1: Call the primary again until it succeeds
1956 $provider = $this->getAuthenticationProvider( $state['primary'] );
1957 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1958 // Configuration changed? Force them to start over.
1959 // @codeCoverageIgnoreStart
1960 $ret = AuthenticationResponse
::newFail(
1961 wfMessage( 'authmanager-link-not-in-progress' )
1963 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1964 $session->remove( 'AuthManager::accountLinkState' );
1966 // @codeCoverageIgnoreEnd
1968 $id = $provider->getUniqueId();
1969 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1970 switch ( $res->status
) {
1971 case AuthenticationResponse
::PASS
;
1972 $this->logger
->info( "Account linked to {user} by $id", [
1973 'user' => $user->getName(),
1975 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1976 $session->remove( 'AuthManager::accountLinkState' );
1978 case AuthenticationResponse
::FAIL
;
1979 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1980 'user' => $user->getName(),
1982 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1983 $session->remove( 'AuthManager::accountLinkState' );
1985 case AuthenticationResponse
::REDIRECT
;
1986 case AuthenticationResponse
::UI
;
1987 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1988 'user' => $user->getName(),
1990 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1991 $state['continueRequests'] = $res->neededRequests
;
1992 $session->setSecret( 'AuthManager::accountLinkState', $state );
1995 throw new \
DomainException(
1996 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1999 } catch ( \Exception
$ex ) {
2000 $session->remove( 'AuthManager::accountLinkState' );
2008 * @name Information methods
2013 * Return the applicable list of AuthenticationRequests
2015 * Possible values for $action:
2016 * - ACTION_LOGIN: Valid for passing to beginAuthentication
2017 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
2018 * - ACTION_CREATE: Valid for passing to beginAccountCreation
2019 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
2020 * - ACTION_LINK: Valid for passing to beginAccountLink
2021 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
2022 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
2023 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
2024 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2026 * @param string $action One of the AuthManager::ACTION_* constants
2027 * @param User|null $user User being acted on, instead of the current user.
2028 * @return AuthenticationRequest[]
2030 public function getAuthenticationRequests( $action, User
$user = null ) {
2032 $providerAction = $action;
2034 // Figure out which providers to query
2035 switch ( $action ) {
2036 case self
::ACTION_LOGIN
:
2037 case self
::ACTION_CREATE
:
2038 $providers = $this->getPreAuthenticationProviders() +
2039 $this->getPrimaryAuthenticationProviders() +
2040 $this->getSecondaryAuthenticationProviders();
2043 case self
::ACTION_LOGIN_CONTINUE
:
2044 $state = $this->request
->getSession()->getSecret( 'AuthManager::authnState' );
2045 return is_array( $state ) ?
$state['continueRequests'] : [];
2047 case self
::ACTION_CREATE_CONTINUE
:
2048 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
2049 return is_array( $state ) ?
$state['continueRequests'] : [];
2051 case self
::ACTION_LINK
:
2052 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2053 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2057 case self
::ACTION_UNLINK
:
2058 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2059 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2062 // To providers, unlink and remove are identical.
2063 $providerAction = self
::ACTION_REMOVE
;
2066 case self
::ACTION_LINK_CONTINUE
:
2067 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' );
2068 return is_array( $state ) ?
$state['continueRequests'] : [];
2070 case self
::ACTION_CHANGE
:
2071 case self
::ACTION_REMOVE
:
2072 $providers = $this->getPrimaryAuthenticationProviders() +
2073 $this->getSecondaryAuthenticationProviders();
2076 // @codeCoverageIgnoreStart
2078 throw new \
DomainException( __METHOD__
. ": Invalid action \"$action\"" );
2080 // @codeCoverageIgnoreEnd
2082 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2086 * Internal request lookup for self::getAuthenticationRequests
2088 * @param string $providerAction Action to pass to providers
2089 * @param array $options Options to pass to providers
2090 * @param AuthenticationProvider[] $providers
2091 * @param User|null $user
2092 * @return AuthenticationRequest[]
2094 private function getAuthenticationRequestsInternal(
2095 $providerAction, array $options, array $providers, User
$user = null
2097 $user = $user ?
: \RequestContext
::getMain()->getUser();
2098 $options['username'] = $user->isAnon() ?
null : $user->getName();
2100 // Query them and merge results
2102 foreach ( $providers as $provider ) {
2103 $isPrimary = $provider instanceof PrimaryAuthenticationProvider
;
2104 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2105 $id = $req->getUniqueId();
2107 // If a required request if from a Primary, mark it as "primary-required" instead
2108 if ( $isPrimary && $req->required
) {
2109 $req->required
= AuthenticationRequest
::PRIMARY_REQUIRED
;
2113 !isset( $reqs[$id] )
2114 ||
$req->required
=== AuthenticationRequest
::REQUIRED
2115 ||
$reqs[$id] === AuthenticationRequest
::OPTIONAL
2122 // AuthManager has its own req for some actions
2123 switch ( $providerAction ) {
2124 case self
::ACTION_LOGIN
:
2125 $reqs[] = new RememberMeAuthenticationRequest
;
2128 case self
::ACTION_CREATE
:
2129 $reqs[] = new UsernameAuthenticationRequest
;
2130 $reqs[] = new UserDataAuthenticationRequest
;
2131 if ( $options['username'] !== null ) {
2132 $reqs[] = new CreationReasonAuthenticationRequest
;
2133 $options['username'] = null; // Don't fill in the username below
2138 // Fill in reqs data
2139 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2141 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2142 if ( $providerAction === self
::ACTION_CHANGE ||
$providerAction === self
::ACTION_REMOVE
) {
2143 $reqs = array_filter( $reqs, function ( $req ) {
2144 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2148 return array_values( $reqs );
2152 * Set values in an array of requests
2153 * @param AuthenticationRequest[] &$reqs
2154 * @param string $action
2155 * @param string|null $username
2156 * @param bool $forceAction
2158 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2159 foreach ( $reqs as $req ) {
2160 if ( !$req->action ||
$forceAction ) {
2161 $req->action
= $action;
2163 if ( $req->username
=== null ) {
2164 $req->username
= $username;
2170 * Determine whether a username exists
2171 * @param string $username
2172 * @param int $flags Bitfield of User:READ_* constants
2175 public function userExists( $username, $flags = User
::READ_NORMAL
) {
2176 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2177 if ( $provider->testUserExists( $username, $flags ) ) {
2186 * Determine whether a user property should be allowed to be changed.
2188 * Supported properties are:
2193 * @param string $property
2196 public function allowsPropertyChange( $property ) {
2197 $providers = $this->getPrimaryAuthenticationProviders() +
2198 $this->getSecondaryAuthenticationProviders();
2199 foreach ( $providers as $provider ) {
2200 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2208 * Get a provider by ID
2209 * @note This is public so extensions can check whether their own provider
2210 * is installed and so they can read its configuration if necessary.
2211 * Other uses are not recommended.
2213 * @return AuthenticationProvider|null
2215 public function getAuthenticationProvider( $id ) {
2217 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2218 return $this->allAuthenticationProviders
[$id];
2221 // Slow version: instantiate each kind and check
2222 $providers = $this->getPrimaryAuthenticationProviders();
2223 if ( isset( $providers[$id] ) ) {
2224 return $providers[$id];
2226 $providers = $this->getSecondaryAuthenticationProviders();
2227 if ( isset( $providers[$id] ) ) {
2228 return $providers[$id];
2230 $providers = $this->getPreAuthenticationProviders();
2231 if ( isset( $providers[$id] ) ) {
2232 return $providers[$id];
2241 * @name Internal methods
2246 * Store authentication in the current session
2247 * @protected For use by AuthenticationProviders
2248 * @param string $key
2249 * @param mixed $data Must be serializable
2251 public function setAuthenticationSessionData( $key, $data ) {
2252 $session = $this->request
->getSession();
2253 $arr = $session->getSecret( 'authData' );
2254 if ( !is_array( $arr ) ) {
2258 $session->setSecret( 'authData', $arr );
2262 * Fetch authentication data from the current session
2263 * @protected For use by AuthenticationProviders
2264 * @param string $key
2265 * @param mixed|null $default
2268 public function getAuthenticationSessionData( $key, $default = null ) {
2269 $arr = $this->request
->getSession()->getSecret( 'authData' );
2270 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2278 * Remove authentication data
2279 * @protected For use by AuthenticationProviders
2280 * @param string|null $key If null, all data is removed
2282 public function removeAuthenticationSessionData( $key ) {
2283 $session = $this->request
->getSession();
2284 if ( $key === null ) {
2285 $session->remove( 'authData' );
2287 $arr = $session->getSecret( 'authData' );
2288 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2289 unset( $arr[$key] );
2290 $session->setSecret( 'authData', $arr );
2296 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2297 * @param string $class
2298 * @param array[] $specs
2299 * @return AuthenticationProvider[]
2301 protected function providerArrayFromSpecs( $class, array $specs ) {
2303 foreach ( $specs as &$spec ) {
2304 $spec = [ 'sort2' => $i++
] +
$spec +
[ 'sort' => 0 ];
2307 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2308 usort( $specs, function ( $a, $b ) {
2309 return $a['sort'] <=> $b['sort']
2310 ?
: $a['sort2'] <=> $b['sort2'];
2314 foreach ( $specs as $spec ) {
2315 $provider = ObjectFactory
::getObjectFromSpec( $spec );
2316 if ( !$provider instanceof $class ) {
2317 throw new \
RuntimeException(
2318 "Expected instance of $class, got " . get_class( $provider )
2321 $provider->setLogger( $this->logger
);
2322 $provider->setManager( $this );
2323 $provider->setConfig( $this->config
);
2324 $id = $provider->getUniqueId();
2325 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2326 throw new \
RuntimeException(
2327 "Duplicate specifications for id $id (classes " .
2328 get_class( $provider ) . ' and ' .
2329 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
2332 $this->allAuthenticationProviders
[$id] = $provider;
2333 $ret[$id] = $provider;
2339 * Get the configuration
2342 private function getConfiguration() {
2343 return $this->config
->get( 'AuthManagerConfig' ) ?
: $this->config
->get( 'AuthManagerAutoConfig' );
2347 * Get the list of PreAuthenticationProviders
2348 * @return PreAuthenticationProvider[]
2350 protected function getPreAuthenticationProviders() {
2351 if ( $this->preAuthenticationProviders
=== null ) {
2352 $conf = $this->getConfiguration();
2353 $this->preAuthenticationProviders
= $this->providerArrayFromSpecs(
2354 PreAuthenticationProvider
::class, $conf['preauth']
2357 return $this->preAuthenticationProviders
;
2361 * Get the list of PrimaryAuthenticationProviders
2362 * @return PrimaryAuthenticationProvider[]
2364 protected function getPrimaryAuthenticationProviders() {
2365 if ( $this->primaryAuthenticationProviders
=== null ) {
2366 $conf = $this->getConfiguration();
2367 $this->primaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2368 PrimaryAuthenticationProvider
::class, $conf['primaryauth']
2371 return $this->primaryAuthenticationProviders
;
2375 * Get the list of SecondaryAuthenticationProviders
2376 * @return SecondaryAuthenticationProvider[]
2378 protected function getSecondaryAuthenticationProviders() {
2379 if ( $this->secondaryAuthenticationProviders
=== null ) {
2380 $conf = $this->getConfiguration();
2381 $this->secondaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2382 SecondaryAuthenticationProvider
::class, $conf['secondaryauth']
2385 return $this->secondaryAuthenticationProviders
;
2391 * @param bool|null $remember
2393 private function setSessionDataForUser( $user, $remember = null ) {
2394 $session = $this->request
->getSession();
2395 $delay = $session->delaySave();
2397 $session->resetId();
2398 $session->resetAllTokens();
2399 if ( $session->canSetUser() ) {
2400 $session->setUser( $user );
2402 if ( $remember !== null ) {
2403 $session->setRememberUser( $remember );
2405 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2406 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2407 $session->persist();
2409 \Wikimedia\ScopedCallback
::consume( $delay );
2411 \Hooks
::run( 'UserLoggedIn', [ $user ] );
2416 * @param bool $useContextLang Use 'uselang' to set the user's language
2418 private function setDefaultUserOptions( User
$user, $useContextLang ) {
2421 $contLang = MediaWikiServices
::getInstance()->getContentLanguage();
2423 $lang = $useContextLang ? \RequestContext
::getMain()->getLanguage() : $contLang;
2424 $user->setOption( 'language', $lang->getPreferredVariant() );
2426 if ( $contLang->hasVariants() ) {
2427 $user->setOption( 'variant', $contLang->getPreferredVariant() );
2432 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2433 * @param string $method
2434 * @param array $args
2436 private function callMethodOnProviders( $which, $method, array $args ) {
2439 $providers +
= $this->getPreAuthenticationProviders();
2442 $providers +
= $this->getPrimaryAuthenticationProviders();
2445 $providers +
= $this->getSecondaryAuthenticationProviders();
2447 foreach ( $providers as $provider ) {
2448 $provider->$method( ...$args );
2453 * Reset the internal caching for unit testing
2454 * @protected Unit tests only
2456 public static function resetCache() {
2457 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2458 // @codeCoverageIgnoreStart
2459 throw new \
MWException( __METHOD__
. ' may only be called from unit tests!' );
2460 // @codeCoverageIgnoreEnd
2463 self
::$instance = null;
2471 * For really cool vim folding this needs to be at the end:
2472 * vim: foldmarker=@{,@} foldmethod=marker