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\MediaWikiServices
;
28 use Psr\Log\LoggerAwareInterface
;
29 use Psr\Log\LoggerInterface
;
34 use Wikimedia\ObjectFactory
;
37 * This serves as the entry point to the authentication system.
39 * In the future, it may also serve as the entry point to the authorization
42 * If you are looking at this because you are working on an extension that creates its own
43 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
44 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
45 * or the createaccount API. Trying to call this class directly will very likely end up in
46 * security vulnerabilities or broken UX in edge cases.
48 * If you are working on an extension that needs to integrate with the authentication system
49 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
50 * need to write an AuthenticationProvider.
52 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
53 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
54 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
55 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
56 * responsibility to ensure that the user can authenticate somehow (see especially
57 * PrimaryAuthenticationProvider::autoCreatedAccount()). The same functionality can also be used
58 * from Maintenance scripts such as createAndPromote.php.
59 * If you are writing code that is not associated with such a provider and needs to create accounts
60 * programmatically for real users, you should rethink your architecture. There is no good way to
61 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
62 * cannot provide any means for users to access the accounts it would create.
64 * The two main control flows when using this class are as follows:
65 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
66 * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
67 * exposing a form specification via the API, so that the client can build it), and pass them to
68 * the appropriate begin* method. That will return either a success/failure response, or more
69 * requests to fill (either by building a form or by redirecting the user to some external
70 * provider which will send the data back), in which case they need to be submitted to the
71 * appropriate continue* method and that step has to be repeated until the response is a success
72 * or failure response. AuthManager will use the session to maintain internal state during the
74 * * Code doing an authentication data change will call getAuthenticationRequests(), select
75 * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
76 * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
77 * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
82 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
84 class AuthManager
implements LoggerAwareInterface
{
85 /** Log in with an existing (not necessarily local) user */
86 const ACTION_LOGIN
= 'login';
87 /** Continue a login process that was interrupted by the need for user input or communication
88 * with an external provider */
89 const ACTION_LOGIN_CONTINUE
= 'login-continue';
90 /** Create a new user */
91 const ACTION_CREATE
= 'create';
92 /** Continue a user creation process that was interrupted by the need for user input or
93 * communication with an external provider */
94 const ACTION_CREATE_CONTINUE
= 'create-continue';
95 /** Link an existing user to a third-party account */
96 const ACTION_LINK
= 'link';
97 /** Continue a user linking process that was interrupted by the need for user input or
98 * communication with an external provider */
99 const ACTION_LINK_CONTINUE
= 'link-continue';
100 /** Change a user's credentials */
101 const ACTION_CHANGE
= 'change';
102 /** Remove a user's credentials */
103 const ACTION_REMOVE
= 'remove';
104 /** Like ACTION_REMOVE but for linking providers only */
105 const ACTION_UNLINK
= 'unlink';
107 /** Security-sensitive operations are ok. */
109 /** Security-sensitive operations should re-authenticate. */
110 const SEC_REAUTH
= 'reauth';
111 /** Security-sensitive should not be performed. */
112 const SEC_FAIL
= 'fail';
114 /** Auto-creation is due to SessionManager */
115 const AUTOCREATE_SOURCE_SESSION
= \MediaWiki\Session\SessionManager
::class;
117 /** Auto-creation is due to a Maintenance script */
118 const AUTOCREATE_SOURCE_MAINT
= '::Maintenance::';
120 /** @var AuthManager|null */
121 private static $instance = null;
123 /** @var WebRequest */
129 /** @var LoggerInterface */
132 /** @var AuthenticationProvider[] */
133 private $allAuthenticationProviders = [];
135 /** @var PreAuthenticationProvider[] */
136 private $preAuthenticationProviders = null;
138 /** @var PrimaryAuthenticationProvider[] */
139 private $primaryAuthenticationProviders = null;
141 /** @var SecondaryAuthenticationProvider[] */
142 private $secondaryAuthenticationProviders = null;
144 /** @var CreatedAccountAuthenticationRequest[] */
145 private $createdAccountAuthenticationRequests = [];
148 * Get the global AuthManager
149 * @return AuthManager
151 public static function singleton() {
152 if ( self
::$instance === null ) {
153 self
::$instance = new self(
154 \RequestContext
::getMain()->getRequest(),
155 MediaWikiServices
::getInstance()->getMainConfig()
158 return self
::$instance;
162 * @param WebRequest $request
163 * @param Config $config
165 public function __construct( WebRequest
$request, Config
$config ) {
166 $this->request
= $request;
167 $this->config
= $config;
168 $this->setLogger( \MediaWiki\Logger\LoggerFactory
::getInstance( 'authentication' ) );
172 * @param LoggerInterface $logger
174 public function setLogger( LoggerInterface
$logger ) {
175 $this->logger
= $logger;
181 public function getRequest() {
182 return $this->request
;
186 * Force certain PrimaryAuthenticationProviders
187 * @deprecated For backwards compatibility only
188 * @param PrimaryAuthenticationProvider[] $providers
191 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
192 $this->logger
->warning( "Overriding AuthManager primary authn because $why" );
194 if ( $this->primaryAuthenticationProviders
!== null ) {
195 $this->logger
->warning(
196 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
199 $this->allAuthenticationProviders
= array_diff_key(
200 $this->allAuthenticationProviders
,
201 $this->primaryAuthenticationProviders
203 $session = $this->request
->getSession();
204 $session->remove( 'AuthManager::authnState' );
205 $session->remove( 'AuthManager::accountCreationState' );
206 $session->remove( 'AuthManager::accountLinkState' );
207 $this->createdAccountAuthenticationRequests
= [];
210 $this->primaryAuthenticationProviders
= [];
211 foreach ( $providers as $provider ) {
212 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
213 throw new \
RuntimeException(
214 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
215 get_class( $provider )
218 $provider->setLogger( $this->logger
);
219 $provider->setManager( $this );
220 $provider->setConfig( $this->config
);
221 $id = $provider->getUniqueId();
222 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
223 throw new \
RuntimeException(
224 "Duplicate specifications for id $id (classes " .
225 get_class( $provider ) . ' and ' .
226 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
229 $this->allAuthenticationProviders
[$id] = $provider;
230 $this->primaryAuthenticationProviders
[$id] = $provider;
235 * Call a legacy AuthPlugin method, if necessary
236 * @codeCoverageIgnore
237 * @deprecated For backwards compatibility only, should be avoided in new code
238 * @param string $method AuthPlugin method to call
239 * @param array $params Parameters to pass
240 * @param mixed $return Return value if AuthPlugin wasn't called
241 * @return mixed Return value from the AuthPlugin method, or $return
243 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
246 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin
) {
247 return $wgAuth->$method( ...$params );
254 * @name Authentication
259 * Indicate whether user authentication is possible
261 * It may not be if the session is provided by something like OAuth
262 * for which each individual request includes authentication data.
266 public function canAuthenticateNow() {
267 return $this->request
->getSession()->canSetUser();
271 * Start an authentication flow
273 * In addition to the AuthenticationRequests returned by
274 * $this->getAuthenticationRequests(), a client might include a
275 * CreateFromLoginAuthenticationRequest from a previous login attempt to
278 * Instead of the AuthenticationRequests returned by
279 * $this->getAuthenticationRequests(), a client might pass a
280 * CreatedAccountAuthenticationRequest from an account creation that just
281 * succeeded to log in to the just-created account.
283 * @param AuthenticationRequest[] $reqs
284 * @param string $returnToUrl Url that REDIRECT responses should eventually
286 * @return AuthenticationResponse See self::continueAuthentication()
288 public function beginAuthentication( array $reqs, $returnToUrl ) {
289 $session = $this->request
->getSession();
290 if ( !$session->canSetUser() ) {
291 // Caller should have called canAuthenticateNow()
292 $session->remove( 'AuthManager::authnState' );
293 throw new \
LogicException( 'Authentication is not possible now' );
296 $guessUserName = null;
297 foreach ( $reqs as $req ) {
298 $req->returnToUrl
= $returnToUrl;
299 // @codeCoverageIgnoreStart
300 if ( $req->username
!== null && $req->username
!== '' ) {
301 if ( $guessUserName === null ) {
302 $guessUserName = $req->username
;
303 } elseif ( $guessUserName !== $req->username
) {
304 $guessUserName = null;
308 // @codeCoverageIgnoreEnd
311 // Check for special-case login of a just-created account
312 $req = AuthenticationRequest
::getRequestByClass(
313 $reqs, CreatedAccountAuthenticationRequest
::class
316 if ( !in_array( $req, $this->createdAccountAuthenticationRequests
, true ) ) {
317 throw new \
LogicException(
318 'CreatedAccountAuthenticationRequests are only valid on ' .
319 'the same AuthManager that created the account'
323 $user = User
::newFromName( $req->username
);
324 // @codeCoverageIgnoreStart
326 throw new \
UnexpectedValueException(
327 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
329 } elseif ( $user->getId() != $req->id
) {
330 throw new \
UnexpectedValueException(
331 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
334 // @codeCoverageIgnoreEnd
336 $this->logger
->info( 'Logging in {user} after account creation', [
337 'user' => $user->getName(),
339 $ret = AuthenticationResponse
::newPass( $user->getName() );
340 $this->setSessionDataForUser( $user );
341 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
342 $session->remove( 'AuthManager::authnState' );
343 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
347 $this->removeAuthenticationSessionData( null );
349 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
350 $status = $provider->testForAuthentication( $reqs );
351 if ( !$status->isGood() ) {
352 $this->logger
->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
353 $ret = AuthenticationResponse
::newFail(
354 Status
::wrap( $status )->getMessage()
356 $this->callMethodOnProviders( 7, 'postAuthentication',
357 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
359 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName, [] ] );
366 'returnToUrl' => $returnToUrl,
367 'guessUserName' => $guessUserName,
369 'primaryResponse' => null,
372 'continueRequests' => [],
375 // Preserve state from a previous failed login
376 $req = AuthenticationRequest
::getRequestByClass(
377 $reqs, CreateFromLoginAuthenticationRequest
::class
380 $state['maybeLink'] = $req->maybeLink
;
383 $session = $this->request
->getSession();
384 $session->setSecret( 'AuthManager::authnState', $state );
387 return $this->continueAuthentication( $reqs );
391 * Continue an authentication flow
393 * Return values are interpreted as follows:
394 * - status FAIL: Authentication failed. If $response->createRequest is
395 * set, that may be passed to self::beginAuthentication() or to
396 * self::beginAccountCreation() to preserve state.
397 * - status REDIRECT: The client should be redirected to the contained URL,
398 * new AuthenticationRequests should be made (if any), then
399 * AuthManager::continueAuthentication() should be called.
400 * - status UI: The client should be presented with a user interface for
401 * the fields in the specified AuthenticationRequests, then new
402 * AuthenticationRequests should be made, then
403 * AuthManager::continueAuthentication() should be called.
404 * - status RESTART: The user logged in successfully with a third-party
405 * service, but the third-party credentials aren't attached to any local
406 * account. This could be treated as a UI or a FAIL.
407 * - status PASS: Authentication was successful.
409 * @param AuthenticationRequest[] $reqs
410 * @return AuthenticationResponse
412 public function continueAuthentication( array $reqs ) {
413 $session = $this->request
->getSession();
415 if ( !$session->canSetUser() ) {
416 // Caller should have called canAuthenticateNow()
417 // @codeCoverageIgnoreStart
418 throw new \
LogicException( 'Authentication is not possible now' );
419 // @codeCoverageIgnoreEnd
422 $state = $session->getSecret( 'AuthManager::authnState' );
423 if ( !is_array( $state ) ) {
424 return AuthenticationResponse
::newFail(
425 wfMessage( 'authmanager-authn-not-in-progress' )
428 $state['continueRequests'] = [];
430 $guessUserName = $state['guessUserName'];
432 foreach ( $reqs as $req ) {
433 $req->returnToUrl
= $state['returnToUrl'];
436 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
438 if ( $state['primary'] === null ) {
439 // We haven't picked a PrimaryAuthenticationProvider yet
440 // @codeCoverageIgnoreStart
441 $guessUserName = null;
442 foreach ( $reqs as $req ) {
443 if ( $req->username
!== null && $req->username
!== '' ) {
444 if ( $guessUserName === null ) {
445 $guessUserName = $req->username
;
446 } elseif ( $guessUserName !== $req->username
) {
447 $guessUserName = null;
452 $state['guessUserName'] = $guessUserName;
453 // @codeCoverageIgnoreEnd
454 $state['reqs'] = $reqs;
456 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
457 $res = $provider->beginPrimaryAuthentication( $reqs );
458 switch ( $res->status
) {
459 case AuthenticationResponse
::PASS
;
460 $state['primary'] = $id;
461 $state['primaryResponse'] = $res;
462 $this->logger
->debug( "Primary login with $id succeeded" );
464 case AuthenticationResponse
::FAIL
;
465 $this->logger
->debug( "Login failed in primary authentication by $id" );
466 if ( $res->createRequest ||
$state['maybeLink'] ) {
467 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
468 $res->createRequest
, $state['maybeLink']
471 $this->callMethodOnProviders( 7, 'postAuthentication',
472 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
474 $session->remove( 'AuthManager::authnState' );
475 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
477 case AuthenticationResponse
::ABSTAIN
;
480 case AuthenticationResponse
::REDIRECT
;
481 case AuthenticationResponse
::UI
;
482 $this->logger
->debug( "Primary login with $id returned $res->status" );
483 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
484 $state['primary'] = $id;
485 $state['continueRequests'] = $res->neededRequests
;
486 $session->setSecret( 'AuthManager::authnState', $state );
489 // @codeCoverageIgnoreStart
491 throw new \
DomainException(
492 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
494 // @codeCoverageIgnoreEnd
497 if ( $state['primary'] === null ) {
498 $this->logger
->debug( 'Login failed in primary authentication because no provider accepted' );
499 $ret = AuthenticationResponse
::newFail(
500 wfMessage( 'authmanager-authn-no-primary' )
502 $this->callMethodOnProviders( 7, 'postAuthentication',
503 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
505 $session->remove( 'AuthManager::authnState' );
508 } elseif ( $state['primaryResponse'] === null ) {
509 $provider = $this->getAuthenticationProvider( $state['primary'] );
510 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
511 // Configuration changed? Force them to start over.
512 // @codeCoverageIgnoreStart
513 $ret = AuthenticationResponse
::newFail(
514 wfMessage( 'authmanager-authn-not-in-progress' )
516 $this->callMethodOnProviders( 7, 'postAuthentication',
517 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
519 $session->remove( 'AuthManager::authnState' );
521 // @codeCoverageIgnoreEnd
523 $id = $provider->getUniqueId();
524 $res = $provider->continuePrimaryAuthentication( $reqs );
525 switch ( $res->status
) {
526 case AuthenticationResponse
::PASS
;
527 $state['primaryResponse'] = $res;
528 $this->logger
->debug( "Primary login with $id succeeded" );
530 case AuthenticationResponse
::FAIL
;
531 $this->logger
->debug( "Login failed in primary authentication by $id" );
532 if ( $res->createRequest ||
$state['maybeLink'] ) {
533 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
534 $res->createRequest
, $state['maybeLink']
537 $this->callMethodOnProviders( 7, 'postAuthentication',
538 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
540 $session->remove( 'AuthManager::authnState' );
541 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName, [] ] );
543 case AuthenticationResponse
::REDIRECT
;
544 case AuthenticationResponse
::UI
;
545 $this->logger
->debug( "Primary login with $id returned $res->status" );
546 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
547 $state['continueRequests'] = $res->neededRequests
;
548 $session->setSecret( 'AuthManager::authnState', $state );
551 throw new \
DomainException(
552 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
557 $res = $state['primaryResponse'];
558 if ( $res->username
=== null ) {
559 $provider = $this->getAuthenticationProvider( $state['primary'] );
560 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
561 // Configuration changed? Force them to start over.
562 // @codeCoverageIgnoreStart
563 $ret = AuthenticationResponse
::newFail(
564 wfMessage( 'authmanager-authn-not-in-progress' )
566 $this->callMethodOnProviders( 7, 'postAuthentication',
567 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
569 $session->remove( 'AuthManager::authnState' );
571 // @codeCoverageIgnoreEnd
574 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
&&
576 // don't confuse the user with an incorrect message if linking is disabled
577 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider
::class )
579 $state['maybeLink'][$res->linkRequest
->getUniqueId()] = $res->linkRequest
;
580 $msg = 'authmanager-authn-no-local-user-link';
582 $msg = 'authmanager-authn-no-local-user';
584 $this->logger
->debug(
585 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
587 $ret = AuthenticationResponse
::newRestart( wfMessage( $msg ) );
588 $ret->neededRequests
= $this->getAuthenticationRequestsInternal(
591 $this->getPrimaryAuthenticationProviders() +
$this->getSecondaryAuthenticationProviders()
593 if ( $res->createRequest ||
$state['maybeLink'] ) {
594 $ret->createRequest
= new CreateFromLoginAuthenticationRequest(
595 $res->createRequest
, $state['maybeLink']
597 $ret->neededRequests
[] = $ret->createRequest
;
599 $this->fillRequests( $ret->neededRequests
, self
::ACTION_LOGIN
, null, true );
600 $session->setSecret( 'AuthManager::authnState', [
601 'reqs' => [], // Will be filled in later
603 'primaryResponse' => null,
605 'continueRequests' => $ret->neededRequests
,
610 // Step 2: Primary authentication succeeded, create the User object
611 // (and add the user locally if necessary)
613 $user = User
::newFromName( $res->username
, 'usable' );
615 $provider = $this->getAuthenticationProvider( $state['primary'] );
616 throw new \
DomainException(
617 get_class( $provider ) . " returned an invalid username: {$res->username}"
620 if ( $user->getId() === 0 ) {
621 // User doesn't exist locally. Create it.
622 $this->logger
->info( 'Auto-creating {user} on login', [
623 'user' => $user->getName(),
625 $status = $this->autoCreateUser( $user, $state['primary'], false );
626 if ( !$status->isGood() ) {
627 $ret = AuthenticationResponse
::newFail(
628 Status
::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
630 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
631 $session->remove( 'AuthManager::authnState' );
632 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
637 // Step 3: Iterate over all the secondary authentication providers.
639 $beginReqs = $state['reqs'];
641 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
642 if ( !isset( $state['secondary'][$id] ) ) {
643 // This provider isn't started yet, so we pass it the set
644 // of reqs from beginAuthentication instead of whatever
645 // might have been used by a previous provider in line.
646 $func = 'beginSecondaryAuthentication';
647 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
648 } elseif ( !$state['secondary'][$id] ) {
649 $func = 'continueSecondaryAuthentication';
650 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
654 switch ( $res->status
) {
655 case AuthenticationResponse
::PASS
;
656 $this->logger
->debug( "Secondary login with $id succeeded" );
658 case AuthenticationResponse
::ABSTAIN
;
659 $state['secondary'][$id] = true;
661 case AuthenticationResponse
::FAIL
;
662 $this->logger
->debug( "Login failed in secondary authentication by $id" );
663 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
664 $session->remove( 'AuthManager::authnState' );
665 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName(), [] ] );
667 case AuthenticationResponse
::REDIRECT
;
668 case AuthenticationResponse
::UI
;
669 $this->logger
->debug( "Secondary login with $id returned " . $res->status
);
670 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $user->getName() );
671 $state['secondary'][$id] = false;
672 $state['continueRequests'] = $res->neededRequests
;
673 $session->setSecret( 'AuthManager::authnState', $state );
676 // @codeCoverageIgnoreStart
678 throw new \
DomainException(
679 get_class( $provider ) . "::{$func}() returned $res->status"
681 // @codeCoverageIgnoreEnd
685 // Step 4: Authentication complete! Set the user in the session and
688 $this->logger
->info( 'Login for {user} succeeded from {clientip}', [
689 'user' => $user->getName(),
690 'clientip' => $this->request
->getIP(),
692 /** @var RememberMeAuthenticationRequest $req */
693 $req = AuthenticationRequest
::getRequestByClass(
694 $beginReqs, RememberMeAuthenticationRequest
::class
696 $this->setSessionDataForUser( $user, $req && $req->rememberMe
);
697 $ret = AuthenticationResponse
::newPass( $user->getName() );
698 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
699 $session->remove( 'AuthManager::authnState' );
700 $this->removeAuthenticationSessionData( null );
701 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName(), [] ] );
703 } catch ( \Exception
$ex ) {
704 $session->remove( 'AuthManager::authnState' );
710 * Whether security-sensitive operations should proceed.
712 * A "security-sensitive operation" is something like a password or email
713 * change, that would normally have a "reenter your password to confirm"
714 * box if we only supported password-based authentication.
716 * @param string $operation Operation being checked. This should be a
717 * message-key-like string such as 'change-password' or 'change-email'.
718 * @return string One of the SEC_* constants.
720 public function securitySensitiveOperationStatus( $operation ) {
721 $status = self
::SEC_OK
;
723 $this->logger
->debug( __METHOD__
. ": Checking $operation" );
725 $session = $this->request
->getSession();
726 $aId = $session->getUser()->getId();
728 // User isn't authenticated. DWIM?
729 $status = $this->canAuthenticateNow() ? self
::SEC_REAUTH
: self
::SEC_FAIL
;
730 $this->logger
->info( __METHOD__
. ": Not logged in! $operation is $status" );
734 if ( $session->canSetUser() ) {
735 $id = $session->get( 'AuthManager:lastAuthId' );
736 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
737 if ( $id !== $aId ||
$last === null ) {
738 $timeSinceLogin = PHP_INT_MAX
; // Forever ago
740 $timeSinceLogin = max( 0, time() - $last );
743 $thresholds = $this->config
->get( 'ReauthenticateTime' );
744 if ( isset( $thresholds[$operation] ) ) {
745 $threshold = $thresholds[$operation];
746 } elseif ( isset( $thresholds['default'] ) ) {
747 $threshold = $thresholds['default'];
749 throw new \
UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
752 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
753 $status = self
::SEC_REAUTH
;
756 $timeSinceLogin = -1;
758 $pass = $this->config
->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
759 if ( isset( $pass[$operation] ) ) {
760 $status = $pass[$operation] ? self
::SEC_OK
: self
::SEC_FAIL
;
761 } elseif ( isset( $pass['default'] ) ) {
762 $status = $pass['default'] ? self
::SEC_OK
: self
::SEC_FAIL
;
764 throw new \
UnexpectedValueException(
765 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
770 \Hooks
::run( 'SecuritySensitiveOperationStatus', [
771 &$status, $operation, $session, $timeSinceLogin
774 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
775 if ( !$this->canAuthenticateNow() && $status === self
::SEC_REAUTH
) {
776 $status = self
::SEC_FAIL
;
779 $this->logger
->info( __METHOD__
. ": $operation is $status for '{user}'",
781 'user' => $session->getUser()->getName(),
782 'clientip' => $this->getRequest()->getIP(),
790 * Determine whether a username can authenticate
792 * This is mainly for internal purposes and only takes authentication data into account,
793 * not things like blocks that can change without the authentication system being aware.
795 * @param string $username MediaWiki username
798 public function userCanAuthenticate( $username ) {
799 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
800 if ( $provider->testUserCanAuthenticate( $username ) ) {
808 * Provide normalized versions of the username for security checks
810 * Since different providers can normalize the input in different ways,
811 * this returns an array of all the different ways the name might be
812 * normalized for authentication.
814 * The returned strings should not be revealed to the user, as that might
815 * leak private information (e.g. an email address might be normalized to a
818 * @param string $username
821 public function normalizeUsername( $username ) {
823 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
824 $normalized = $provider->providerNormalizeUsername( $username );
825 if ( $normalized !== null ) {
826 $ret[$normalized] = true;
829 return array_keys( $ret );
835 * @name Authentication data changing
840 * Revoke any authentication credentials for a user
842 * After this, the user should no longer be able to log in.
844 * @param string $username
846 public function revokeAccessForUser( $username ) {
847 $this->logger
->info( 'Revoking access for {user}', [
850 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
854 * Validate a change of authentication data (e.g. passwords)
855 * @param AuthenticationRequest $req
856 * @param bool $checkData If false, $req hasn't been loaded from the
857 * submission so checks on user-submitted fields should be skipped. $req->username is
858 * considered user-submitted for this purpose, even if it cannot be changed via
859 * $req->loadFromSubmission.
862 public function allowsAuthenticationDataChange( AuthenticationRequest
$req, $checkData = true ) {
864 $providers = $this->getPrimaryAuthenticationProviders() +
865 $this->getSecondaryAuthenticationProviders();
866 foreach ( $providers as $provider ) {
867 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
868 if ( !$status->isGood() ) {
869 return Status
::wrap( $status );
871 $any = $any ||
$status->value
!== 'ignored';
874 $status = Status
::newGood( 'ignored' );
875 $status->warning( 'authmanager-change-not-supported' );
878 return Status
::newGood();
882 * Change authentication data (e.g. passwords)
884 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
885 * result in a successful login in the future.
887 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
888 * no longer result in a successful login.
890 * This method should only be called if allowsAuthenticationDataChange( $req, true )
893 * @param AuthenticationRequest $req
894 * @param bool $isAddition Set true if this represents an addition of
895 * credentials rather than a change. The main difference is that additions
896 * should not invalidate BotPasswords. If you're not sure, leave it false.
898 public function changeAuthenticationData( AuthenticationRequest
$req, $isAddition = false ) {
899 $this->logger
->info( 'Changing authentication data for {user} class {what}', [
900 'user' => is_string( $req->username
) ?
$req->username
: '<no name>',
901 'what' => get_class( $req ),
904 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
906 // When the main account's authentication data is changed, invalidate
907 // all BotPasswords too.
908 if ( !$isAddition ) {
909 \BotPassword
::invalidateAllPasswordsForUser( $req->username
);
916 * @name Account creation
921 * Determine whether accounts can be created
924 public function canCreateAccounts() {
925 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
926 switch ( $provider->accountCreationType() ) {
927 case PrimaryAuthenticationProvider
::TYPE_CREATE
:
928 case PrimaryAuthenticationProvider
::TYPE_LINK
:
936 * Determine whether a particular account can be created
937 * @param string $username MediaWiki username
938 * @param array $options
939 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
940 * - creating: (bool) For internal use only. Never specify this.
943 public function canCreateAccount( $username, $options = [] ) {
945 if ( is_int( $options ) ) {
946 $options = [ 'flags' => $options ];
949 'flags' => User
::READ_NORMAL
,
952 $flags = $options['flags'];
954 if ( !$this->canCreateAccounts() ) {
955 return Status
::newFatal( 'authmanager-create-disabled' );
958 if ( $this->userExists( $username, $flags ) ) {
959 return Status
::newFatal( 'userexists' );
962 $user = User
::newFromName( $username, 'creatable' );
963 if ( !is_object( $user ) ) {
964 return Status
::newFatal( 'noname' );
966 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
967 if ( $user->getId() !== 0 ) {
968 return Status
::newFatal( 'userexists' );
972 // Denied by providers?
973 $providers = $this->getPreAuthenticationProviders() +
974 $this->getPrimaryAuthenticationProviders() +
975 $this->getSecondaryAuthenticationProviders();
976 foreach ( $providers as $provider ) {
977 $status = $provider->testUserForCreation( $user, false, $options );
978 if ( !$status->isGood() ) {
979 return Status
::wrap( $status );
983 return Status
::newGood();
987 * Basic permissions checks on whether a user can create accounts
988 * @param User $creator User doing the account creation
991 public function checkAccountCreatePermissions( User
$creator ) {
992 // Wiki is read-only?
993 if ( wfReadOnly() ) {
994 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
997 // This is awful, this permission check really shouldn't go through Title.
998 $permErrors = \SpecialPage
::getTitleFor( 'CreateAccount' )
999 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
1000 if ( $permErrors ) {
1001 $status = Status
::newGood();
1002 foreach ( $permErrors as $args ) {
1003 $status->fatal( ...$args );
1008 $block = $creator->isBlockedFromCreateAccount();
1011 $block->getTarget(),
1012 $block->mReason ?
: wfMessage( 'blockednoreason' )->text(),
1016 if ( $block->getType() === \Block
::TYPE_RANGE
) {
1017 $errorMessage = 'cantcreateaccount-range-text';
1018 $errorParams[] = $this->getRequest()->getIP();
1020 $errorMessage = 'cantcreateaccount-text';
1023 return Status
::newFatal( wfMessage( $errorMessage, $errorParams ) );
1026 $ip = $this->getRequest()->getIP();
1027 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1028 return Status
::newFatal( 'sorbs_create_account_reason' );
1031 return Status
::newGood();
1035 * Start an account creation flow
1037 * In addition to the AuthenticationRequests returned by
1038 * $this->getAuthenticationRequests(), a client might include a
1039 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1041 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1043 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1044 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1045 * username set, that username must be used for all other requests.
1047 * @param User $creator User doing the account creation
1048 * @param AuthenticationRequest[] $reqs
1049 * @param string $returnToUrl Url that REDIRECT responses should eventually
1051 * @return AuthenticationResponse
1053 public function beginAccountCreation( User
$creator, array $reqs, $returnToUrl ) {
1054 $session = $this->request
->getSession();
1055 if ( !$this->canCreateAccounts() ) {
1056 // Caller should have called canCreateAccounts()
1057 $session->remove( 'AuthManager::accountCreationState' );
1058 throw new \
LogicException( 'Account creation is not possible' );
1062 $username = AuthenticationRequest
::getUsernameFromRequests( $reqs );
1063 } catch ( \UnexpectedValueException
$ex ) {
1066 if ( $username === null ) {
1067 $this->logger
->debug( __METHOD__
. ': No username provided' );
1068 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1071 // Permissions check
1072 $status = $this->checkAccountCreatePermissions( $creator );
1073 if ( !$status->isGood() ) {
1074 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1075 'user' => $username,
1076 'creator' => $creator->getName(),
1077 'reason' => $status->getWikiText( null, null, 'en' )
1079 return AuthenticationResponse
::newFail( $status->getMessage() );
1082 $status = $this->canCreateAccount(
1083 $username, [ 'flags' => User
::READ_LOCKING
, 'creating' => true ]
1085 if ( !$status->isGood() ) {
1086 $this->logger
->debug( __METHOD__
. ': {user} cannot be created: {reason}', [
1087 'user' => $username,
1088 'creator' => $creator->getName(),
1089 'reason' => $status->getWikiText( null, null, 'en' )
1091 return AuthenticationResponse
::newFail( $status->getMessage() );
1094 $user = User
::newFromName( $username, 'creatable' );
1095 foreach ( $reqs as $req ) {
1096 $req->username
= $username;
1097 $req->returnToUrl
= $returnToUrl;
1098 if ( $req instanceof UserDataAuthenticationRequest
) {
1099 $status = $req->populateUser( $user );
1100 if ( !$status->isGood() ) {
1101 $status = Status
::wrap( $status );
1102 $session->remove( 'AuthManager::accountCreationState' );
1103 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1104 'user' => $user->getName(),
1105 'creator' => $creator->getName(),
1106 'reason' => $status->getWikiText( null, null, 'en' ),
1108 return AuthenticationResponse
::newFail( $status->getMessage() );
1113 $this->removeAuthenticationSessionData( null );
1116 'username' => $username,
1118 'creatorid' => $creator->getId(),
1119 'creatorname' => $creator->getName(),
1121 'returnToUrl' => $returnToUrl,
1123 'primaryResponse' => null,
1125 'continueRequests' => [],
1127 'ranPreTests' => false,
1130 // Special case: converting a login to an account creation
1131 $req = AuthenticationRequest
::getRequestByClass(
1132 $reqs, CreateFromLoginAuthenticationRequest
::class
1135 $state['maybeLink'] = $req->maybeLink
;
1137 if ( $req->createRequest
) {
1138 $reqs[] = $req->createRequest
;
1139 $state['reqs'][] = $req->createRequest
;
1143 $session->setSecret( 'AuthManager::accountCreationState', $state );
1144 $session->persist();
1146 return $this->continueAccountCreation( $reqs );
1150 * Continue an account creation flow
1151 * @param AuthenticationRequest[] $reqs
1152 * @return AuthenticationResponse
1154 public function continueAccountCreation( array $reqs ) {
1155 $session = $this->request
->getSession();
1157 if ( !$this->canCreateAccounts() ) {
1158 // Caller should have called canCreateAccounts()
1159 $session->remove( 'AuthManager::accountCreationState' );
1160 throw new \
LogicException( 'Account creation is not possible' );
1163 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1164 if ( !is_array( $state ) ) {
1165 return AuthenticationResponse
::newFail(
1166 wfMessage( 'authmanager-create-not-in-progress' )
1169 $state['continueRequests'] = [];
1171 // Step 0: Prepare and validate the input
1173 $user = User
::newFromName( $state['username'], 'creatable' );
1174 if ( !is_object( $user ) ) {
1175 $session->remove( 'AuthManager::accountCreationState' );
1176 $this->logger
->debug( __METHOD__
. ': Invalid username', [
1177 'user' => $state['username'],
1179 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1182 if ( $state['creatorid'] ) {
1183 $creator = User
::newFromId( $state['creatorid'] );
1185 $creator = new User
;
1186 $creator->setName( $state['creatorname'] );
1189 // Avoid account creation races on double submissions
1190 $cache = \ObjectCache
::getLocalClusterInstance();
1191 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1193 // Don't clear AuthManager::accountCreationState for this code
1194 // path because the process that won the race owns it.
1195 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1196 'user' => $user->getName(),
1197 'creator' => $creator->getName(),
1199 return AuthenticationResponse
::newFail( wfMessage( 'usernameinprogress' ) );
1202 // Permissions check
1203 $status = $this->checkAccountCreatePermissions( $creator );
1204 if ( !$status->isGood() ) {
1205 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1206 'user' => $user->getName(),
1207 'creator' => $creator->getName(),
1208 'reason' => $status->getWikiText( null, null, 'en' )
1210 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1211 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1212 $session->remove( 'AuthManager::accountCreationState' );
1216 // Load from master for existence check
1217 $user->load( User
::READ_LOCKING
);
1219 if ( $state['userid'] === 0 ) {
1220 if ( $user->getId() !== 0 ) {
1221 $this->logger
->debug( __METHOD__
. ': User exists locally', [
1222 'user' => $user->getName(),
1223 'creator' => $creator->getName(),
1225 $ret = AuthenticationResponse
::newFail( wfMessage( 'userexists' ) );
1226 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1227 $session->remove( 'AuthManager::accountCreationState' );
1231 if ( $user->getId() === 0 ) {
1232 $this->logger
->debug( __METHOD__
. ': User does not exist locally when it should', [
1233 'user' => $user->getName(),
1234 'creator' => $creator->getName(),
1235 'expected_id' => $state['userid'],
1237 throw new \
UnexpectedValueException(
1238 "User \"{$state['username']}\" should exist now, but doesn't!"
1241 if ( $user->getId() !== $state['userid'] ) {
1242 $this->logger
->debug( __METHOD__
. ': User ID/name mismatch', [
1243 'user' => $user->getName(),
1244 'creator' => $creator->getName(),
1245 'expected_id' => $state['userid'],
1246 'actual_id' => $user->getId(),
1248 throw new \
UnexpectedValueException(
1249 "User \"{$state['username']}\" exists, but " .
1250 "ID {$user->getId()} !== {$state['userid']}!"
1254 foreach ( $state['reqs'] as $req ) {
1255 if ( $req instanceof UserDataAuthenticationRequest
) {
1256 $status = $req->populateUser( $user );
1257 if ( !$status->isGood() ) {
1258 // This should never happen...
1259 $status = Status
::wrap( $status );
1260 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1261 'user' => $user->getName(),
1262 'creator' => $creator->getName(),
1263 'reason' => $status->getWikiText( null, null, 'en' ),
1265 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1266 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1267 $session->remove( 'AuthManager::accountCreationState' );
1273 foreach ( $reqs as $req ) {
1274 $req->returnToUrl
= $state['returnToUrl'];
1275 $req->username
= $state['username'];
1278 // Run pre-creation tests, if we haven't already
1279 if ( !$state['ranPreTests'] ) {
1280 $providers = $this->getPreAuthenticationProviders() +
1281 $this->getPrimaryAuthenticationProviders() +
1282 $this->getSecondaryAuthenticationProviders();
1283 foreach ( $providers as $id => $provider ) {
1284 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1285 if ( !$status->isGood() ) {
1286 $this->logger
->debug( __METHOD__
. ": Fail in pre-authentication by $id", [
1287 'user' => $user->getName(),
1288 'creator' => $creator->getName(),
1290 $ret = AuthenticationResponse
::newFail(
1291 Status
::wrap( $status )->getMessage()
1293 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1294 $session->remove( 'AuthManager::accountCreationState' );
1299 $state['ranPreTests'] = true;
1302 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1304 if ( $state['primary'] === null ) {
1305 // We haven't picked a PrimaryAuthenticationProvider yet
1306 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1307 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_NONE
) {
1310 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1311 switch ( $res->status
) {
1312 case AuthenticationResponse
::PASS
;
1313 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1314 'user' => $user->getName(),
1315 'creator' => $creator->getName(),
1317 $state['primary'] = $id;
1318 $state['primaryResponse'] = $res;
1320 case AuthenticationResponse
::FAIL
;
1321 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1322 'user' => $user->getName(),
1323 'creator' => $creator->getName(),
1325 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1326 $session->remove( 'AuthManager::accountCreationState' );
1328 case AuthenticationResponse
::ABSTAIN
;
1331 case AuthenticationResponse
::REDIRECT
;
1332 case AuthenticationResponse
::UI
;
1333 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1334 'user' => $user->getName(),
1335 'creator' => $creator->getName(),
1337 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1338 $state['primary'] = $id;
1339 $state['continueRequests'] = $res->neededRequests
;
1340 $session->setSecret( 'AuthManager::accountCreationState', $state );
1343 // @codeCoverageIgnoreStart
1345 throw new \
DomainException(
1346 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1348 // @codeCoverageIgnoreEnd
1351 if ( $state['primary'] === null ) {
1352 $this->logger
->debug( __METHOD__
. ': Primary creation failed because no provider accepted', [
1353 'user' => $user->getName(),
1354 'creator' => $creator->getName(),
1356 $ret = AuthenticationResponse
::newFail(
1357 wfMessage( 'authmanager-create-no-primary' )
1359 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1360 $session->remove( 'AuthManager::accountCreationState' );
1363 } elseif ( $state['primaryResponse'] === null ) {
1364 $provider = $this->getAuthenticationProvider( $state['primary'] );
1365 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1366 // Configuration changed? Force them to start over.
1367 // @codeCoverageIgnoreStart
1368 $ret = AuthenticationResponse
::newFail(
1369 wfMessage( 'authmanager-create-not-in-progress' )
1371 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1372 $session->remove( 'AuthManager::accountCreationState' );
1374 // @codeCoverageIgnoreEnd
1376 $id = $provider->getUniqueId();
1377 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1378 switch ( $res->status
) {
1379 case AuthenticationResponse
::PASS
;
1380 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1381 'user' => $user->getName(),
1382 'creator' => $creator->getName(),
1384 $state['primaryResponse'] = $res;
1386 case AuthenticationResponse
::FAIL
;
1387 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1388 'user' => $user->getName(),
1389 'creator' => $creator->getName(),
1391 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1392 $session->remove( 'AuthManager::accountCreationState' );
1394 case AuthenticationResponse
::REDIRECT
;
1395 case AuthenticationResponse
::UI
;
1396 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1397 'user' => $user->getName(),
1398 'creator' => $creator->getName(),
1400 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1401 $state['continueRequests'] = $res->neededRequests
;
1402 $session->setSecret( 'AuthManager::accountCreationState', $state );
1405 throw new \
DomainException(
1406 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1411 // Step 2: Primary authentication succeeded, create the User object
1412 // and add the user locally.
1414 if ( $state['userid'] === 0 ) {
1415 $this->logger
->info( 'Creating user {user} during account creation', [
1416 'user' => $user->getName(),
1417 'creator' => $creator->getName(),
1419 $status = $user->addToDatabase();
1420 if ( !$status->isOK() ) {
1421 // @codeCoverageIgnoreStart
1422 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1423 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1424 $session->remove( 'AuthManager::accountCreationState' );
1426 // @codeCoverageIgnoreEnd
1428 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1429 \Hooks
::run( 'LocalUserCreated', [ $user, false ] );
1430 $user->saveSettings();
1431 $state['userid'] = $user->getId();
1433 // Update user count
1434 \DeferredUpdates
::addUpdate( \SiteStatsUpdate
::factory( [ 'users' => 1 ] ) );
1436 // Watch user's userpage and talk page
1437 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1439 // Inform the provider
1440 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1443 if ( $this->config
->get( 'NewUserLog' ) ) {
1444 $isAnon = $creator->isAnon();
1445 $logEntry = new \
ManualLogEntry(
1447 $logSubtype ?
: ( $isAnon ?
'create' : 'create2' )
1449 $logEntry->setPerformer( $isAnon ?
$user : $creator );
1450 $logEntry->setTarget( $user->getUserPage() );
1451 /** @var CreationReasonAuthenticationRequest $req */
1452 $req = AuthenticationRequest
::getRequestByClass(
1453 $state['reqs'], CreationReasonAuthenticationRequest
::class
1455 $logEntry->setComment( $req ?
$req->reason
: '' );
1456 $logEntry->setParameters( [
1457 '4::userid' => $user->getId(),
1459 $logid = $logEntry->insert();
1460 $logEntry->publish( $logid );
1464 // Step 3: Iterate over all the secondary authentication providers.
1466 $beginReqs = $state['reqs'];
1468 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1469 if ( !isset( $state['secondary'][$id] ) ) {
1470 // This provider isn't started yet, so we pass it the set
1471 // of reqs from beginAuthentication instead of whatever
1472 // might have been used by a previous provider in line.
1473 $func = 'beginSecondaryAccountCreation';
1474 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1475 } elseif ( !$state['secondary'][$id] ) {
1476 $func = 'continueSecondaryAccountCreation';
1477 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1481 switch ( $res->status
) {
1482 case AuthenticationResponse
::PASS
;
1483 $this->logger
->debug( __METHOD__
. ": Secondary creation passed by $id", [
1484 'user' => $user->getName(),
1485 'creator' => $creator->getName(),
1488 case AuthenticationResponse
::ABSTAIN
;
1489 $state['secondary'][$id] = true;
1491 case AuthenticationResponse
::REDIRECT
;
1492 case AuthenticationResponse
::UI
;
1493 $this->logger
->debug( __METHOD__
. ": Secondary creation $res->status by $id", [
1494 'user' => $user->getName(),
1495 'creator' => $creator->getName(),
1497 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1498 $state['secondary'][$id] = false;
1499 $state['continueRequests'] = $res->neededRequests
;
1500 $session->setSecret( 'AuthManager::accountCreationState', $state );
1502 case AuthenticationResponse
::FAIL
;
1503 throw new \
DomainException(
1504 get_class( $provider ) . "::{$func}() returned $res->status." .
1505 ' Secondary providers are not allowed to fail account creation, that' .
1506 ' should have been done via testForAccountCreation().'
1508 // @codeCoverageIgnoreStart
1510 throw new \
DomainException(
1511 get_class( $provider ) . "::{$func}() returned $res->status"
1513 // @codeCoverageIgnoreEnd
1517 $id = $user->getId();
1518 $name = $user->getName();
1519 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1520 $ret = AuthenticationResponse
::newPass( $name );
1521 $ret->loginRequest
= $req;
1522 $this->createdAccountAuthenticationRequests
[] = $req;
1524 $this->logger
->info( __METHOD__
. ': Account creation succeeded for {user}', [
1525 'user' => $user->getName(),
1526 'creator' => $creator->getName(),
1529 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1530 $session->remove( 'AuthManager::accountCreationState' );
1531 $this->removeAuthenticationSessionData( null );
1533 } catch ( \Exception
$ex ) {
1534 $session->remove( 'AuthManager::accountCreationState' );
1540 * Auto-create an account, and log into that account
1542 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1543 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1544 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1545 * the username of a non-existing user from provideSessionInfo(). Calling this method
1546 * explicitly (e.g. from a maintenance script) is also fine.
1548 * @param User $user User to auto-create
1549 * @param string $source What caused the auto-creation? This must be one of:
1550 * - the ID of a PrimaryAuthenticationProvider,
1551 * - the constant self::AUTOCREATE_SOURCE_SESSION, or
1552 * - the constant AUTOCREATE_SOURCE_MAINT.
1553 * @param bool $login Whether to also log the user in
1554 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1556 public function autoCreateUser( User
$user, $source, $login = true ) {
1557 if ( $source !== self
::AUTOCREATE_SOURCE_SESSION
&&
1558 $source !== self
::AUTOCREATE_SOURCE_MAINT
&&
1559 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1561 throw new \
InvalidArgumentException( "Unknown auto-creation source: $source" );
1564 $username = $user->getName();
1566 // Try the local user from the replica DB
1567 $localId = User
::idFromName( $username );
1568 $flags = User
::READ_NORMAL
;
1570 // Fetch the user ID from the master, so that we don't try to create the user
1571 // when they already exist, due to replication lag
1572 // @codeCoverageIgnoreStart
1575 MediaWikiServices
::getInstance()->getDBLoadBalancer()->getReaderIndex() !== 0
1577 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1578 $flags = User
::READ_LATEST
;
1580 // @codeCoverageIgnoreEnd
1583 $this->logger
->debug( __METHOD__
. ': {username} already exists locally', [
1584 'username' => $username,
1586 $user->setId( $localId );
1587 $user->loadFromId( $flags );
1589 $this->setSessionDataForUser( $user );
1591 $status = Status
::newGood();
1592 $status->warning( 'userexists' );
1596 // Wiki is read-only?
1597 if ( wfReadOnly() ) {
1598 $this->logger
->debug( __METHOD__
. ': denied by wfReadOnly(): {reason}', [
1599 'username' => $username,
1600 'reason' => wfReadOnlyReason(),
1603 $user->loadFromId();
1604 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1607 // Check the session, if we tried to create this user already there's
1608 // no point in retrying.
1609 $session = $this->request
->getSession();
1610 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1611 $this->logger
->debug( __METHOD__
. ': blacklisted in session {sessionid}', [
1612 'username' => $username,
1613 'sessionid' => $session->getId(),
1616 $user->loadFromId();
1617 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1618 if ( $reason instanceof StatusValue
) {
1619 return Status
::wrap( $reason );
1621 return Status
::newFatal( $reason );
1625 // Is the username creatable?
1626 if ( !User
::isCreatableName( $username ) ) {
1627 $this->logger
->debug( __METHOD__
. ': name "{username}" is not creatable', [
1628 'username' => $username,
1630 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1632 $user->loadFromId();
1633 return Status
::newFatal( 'noname' );
1636 // Is the IP user able to create accounts?
1638 if ( $source !== self
::AUTOCREATE_SOURCE_MAINT
&&
1639 !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1641 $this->logger
->debug( __METHOD__
. ': IP lacks the ability to create or autocreate accounts', [
1642 'username' => $username,
1643 'ip' => $anon->getName(),
1645 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1646 $session->persist();
1648 $user->loadFromId();
1649 return Status
::newFatal( 'authmanager-autocreate-noperm' );
1652 // Avoid account creation races on double submissions
1653 $cache = \ObjectCache
::getLocalClusterInstance();
1654 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1656 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1657 'user' => $username,
1660 $user->loadFromId();
1661 return Status
::newFatal( 'usernameinprogress' );
1664 // Denied by providers?
1666 'flags' => User
::READ_LATEST
,
1669 $providers = $this->getPreAuthenticationProviders() +
1670 $this->getPrimaryAuthenticationProviders() +
1671 $this->getSecondaryAuthenticationProviders();
1672 foreach ( $providers as $provider ) {
1673 $status = $provider->testUserForCreation( $user, $source, $options );
1674 if ( !$status->isGood() ) {
1675 $ret = Status
::wrap( $status );
1676 $this->logger
->debug( __METHOD__
. ': Provider denied creation of {username}: {reason}', [
1677 'username' => $username,
1678 'reason' => $ret->getWikiText( null, null, 'en' ),
1680 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1682 $user->loadFromId();
1687 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1688 if ( $cache->get( $backoffKey ) ) {
1689 $this->logger
->debug( __METHOD__
. ': {username} denied by prior creation attempt failures', [
1690 'username' => $username,
1693 $user->loadFromId();
1694 return Status
::newFatal( 'authmanager-autocreate-exception' );
1697 // Checks passed, create the user...
1698 $from = $_SERVER['REQUEST_URI'] ??
'CLI';
1699 $this->logger
->info( __METHOD__
. ': creating new user ({username}) - from: {from}', [
1700 'username' => $username,
1704 // Ignore warnings about master connections/writes...hard to avoid here
1705 $trxProfiler = \Profiler
::instance()->getTransactionProfiler();
1706 $old = $trxProfiler->setSilenced( true );
1708 $status = $user->addToDatabase();
1709 if ( !$status->isOK() ) {
1710 // Double-check for a race condition (T70012). We make use of the fact that when
1711 // addToDatabase fails due to the user already existing, the user object gets loaded.
1712 if ( $user->getId() ) {
1713 $this->logger
->info( __METHOD__
. ': {username} already exists locally (race)', [
1714 'username' => $username,
1717 $this->setSessionDataForUser( $user );
1719 $status = Status
::newGood();
1720 $status->warning( 'userexists' );
1722 $this->logger
->error( __METHOD__
. ': {username} failed with message {msg}', [
1723 'username' => $username,
1724 'msg' => $status->getWikiText( null, null, 'en' )
1727 $user->loadFromId();
1731 } catch ( \Exception
$ex ) {
1732 $trxProfiler->setSilenced( $old );
1733 $this->logger
->error( __METHOD__
. ': {username} failed with exception {exception}', [
1734 'username' => $username,
1737 // Do not keep throwing errors for a while
1738 $cache->set( $backoffKey, 1, 600 );
1739 // Bubble up error; which should normally trigger DB rollbacks
1743 $this->setDefaultUserOptions( $user, false );
1745 // Inform the providers
1746 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1748 \Hooks
::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
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
2109 if ( $req->required
) {
2110 $req->required
= AuthenticationRequest
::PRIMARY_REQUIRED
;
2115 !isset( $reqs[$id] )
2116 ||
$req->required
=== AuthenticationRequest
::REQUIRED
2117 ||
$reqs[$id] === AuthenticationRequest
::OPTIONAL
2124 // AuthManager has its own req for some actions
2125 switch ( $providerAction ) {
2126 case self
::ACTION_LOGIN
:
2127 $reqs[] = new RememberMeAuthenticationRequest
;
2130 case self
::ACTION_CREATE
:
2131 $reqs[] = new UsernameAuthenticationRequest
;
2132 $reqs[] = new UserDataAuthenticationRequest
;
2133 if ( $options['username'] !== null ) {
2134 $reqs[] = new CreationReasonAuthenticationRequest
;
2135 $options['username'] = null; // Don't fill in the username below
2140 // Fill in reqs data
2141 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2143 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2144 if ( $providerAction === self
::ACTION_CHANGE ||
$providerAction === self
::ACTION_REMOVE
) {
2145 $reqs = array_filter( $reqs, function ( $req ) {
2146 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2150 return array_values( $reqs );
2154 * Set values in an array of requests
2155 * @param AuthenticationRequest[] &$reqs
2156 * @param string $action
2157 * @param string|null $username
2158 * @param bool $forceAction
2160 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2161 foreach ( $reqs as $req ) {
2162 if ( !$req->action ||
$forceAction ) {
2163 $req->action
= $action;
2165 if ( $req->username
=== null ) {
2166 $req->username
= $username;
2172 * Determine whether a username exists
2173 * @param string $username
2174 * @param int $flags Bitfield of User:READ_* constants
2177 public function userExists( $username, $flags = User
::READ_NORMAL
) {
2178 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2179 if ( $provider->testUserExists( $username, $flags ) ) {
2188 * Determine whether a user property should be allowed to be changed.
2190 * Supported properties are:
2195 * @param string $property
2198 public function allowsPropertyChange( $property ) {
2199 $providers = $this->getPrimaryAuthenticationProviders() +
2200 $this->getSecondaryAuthenticationProviders();
2201 foreach ( $providers as $provider ) {
2202 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2210 * Get a provider by ID
2211 * @note This is public so extensions can check whether their own provider
2212 * is installed and so they can read its configuration if necessary.
2213 * Other uses are not recommended.
2215 * @return AuthenticationProvider|null
2217 public function getAuthenticationProvider( $id ) {
2219 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2220 return $this->allAuthenticationProviders
[$id];
2223 // Slow version: instantiate each kind and check
2224 $providers = $this->getPrimaryAuthenticationProviders();
2225 if ( isset( $providers[$id] ) ) {
2226 return $providers[$id];
2228 $providers = $this->getSecondaryAuthenticationProviders();
2229 if ( isset( $providers[$id] ) ) {
2230 return $providers[$id];
2232 $providers = $this->getPreAuthenticationProviders();
2233 if ( isset( $providers[$id] ) ) {
2234 return $providers[$id];
2243 * @name Internal methods
2248 * Store authentication in the current session
2249 * @protected For use by AuthenticationProviders
2250 * @param string $key
2251 * @param mixed $data Must be serializable
2253 public function setAuthenticationSessionData( $key, $data ) {
2254 $session = $this->request
->getSession();
2255 $arr = $session->getSecret( 'authData' );
2256 if ( !is_array( $arr ) ) {
2260 $session->setSecret( 'authData', $arr );
2264 * Fetch authentication data from the current session
2265 * @protected For use by AuthenticationProviders
2266 * @param string $key
2267 * @param mixed|null $default
2270 public function getAuthenticationSessionData( $key, $default = null ) {
2271 $arr = $this->request
->getSession()->getSecret( 'authData' );
2272 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2280 * Remove authentication data
2281 * @protected For use by AuthenticationProviders
2282 * @param string|null $key If null, all data is removed
2284 public function removeAuthenticationSessionData( $key ) {
2285 $session = $this->request
->getSession();
2286 if ( $key === null ) {
2287 $session->remove( 'authData' );
2289 $arr = $session->getSecret( 'authData' );
2290 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2291 unset( $arr[$key] );
2292 $session->setSecret( 'authData', $arr );
2298 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2299 * @param string $class
2300 * @param array[] $specs
2301 * @return AuthenticationProvider[]
2303 protected function providerArrayFromSpecs( $class, array $specs ) {
2305 foreach ( $specs as &$spec ) {
2306 $spec = [ 'sort2' => $i++
] +
$spec +
[ 'sort' => 0 ];
2309 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2310 usort( $specs, function ( $a, $b ) {
2311 return $a['sort'] <=> $b['sort']
2312 ?
: $a['sort2'] <=> $b['sort2'];
2316 foreach ( $specs as $spec ) {
2317 $provider = ObjectFactory
::getObjectFromSpec( $spec );
2318 if ( !$provider instanceof $class ) {
2319 throw new \
RuntimeException(
2320 "Expected instance of $class, got " . get_class( $provider )
2323 $provider->setLogger( $this->logger
);
2324 $provider->setManager( $this );
2325 $provider->setConfig( $this->config
);
2326 $id = $provider->getUniqueId();
2327 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2328 throw new \
RuntimeException(
2329 "Duplicate specifications for id $id (classes " .
2330 get_class( $provider ) . ' and ' .
2331 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
2334 $this->allAuthenticationProviders
[$id] = $provider;
2335 $ret[$id] = $provider;
2341 * Get the configuration
2344 private function getConfiguration() {
2345 return $this->config
->get( 'AuthManagerConfig' ) ?
: $this->config
->get( 'AuthManagerAutoConfig' );
2349 * Get the list of PreAuthenticationProviders
2350 * @return PreAuthenticationProvider[]
2352 protected function getPreAuthenticationProviders() {
2353 if ( $this->preAuthenticationProviders
=== null ) {
2354 $conf = $this->getConfiguration();
2355 $this->preAuthenticationProviders
= $this->providerArrayFromSpecs(
2356 PreAuthenticationProvider
::class, $conf['preauth']
2359 return $this->preAuthenticationProviders
;
2363 * Get the list of PrimaryAuthenticationProviders
2364 * @return PrimaryAuthenticationProvider[]
2366 protected function getPrimaryAuthenticationProviders() {
2367 if ( $this->primaryAuthenticationProviders
=== null ) {
2368 $conf = $this->getConfiguration();
2369 $this->primaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2370 PrimaryAuthenticationProvider
::class, $conf['primaryauth']
2373 return $this->primaryAuthenticationProviders
;
2377 * Get the list of SecondaryAuthenticationProviders
2378 * @return SecondaryAuthenticationProvider[]
2380 protected function getSecondaryAuthenticationProviders() {
2381 if ( $this->secondaryAuthenticationProviders
=== null ) {
2382 $conf = $this->getConfiguration();
2383 $this->secondaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2384 SecondaryAuthenticationProvider
::class, $conf['secondaryauth']
2387 return $this->secondaryAuthenticationProviders
;
2393 * @param bool|null $remember
2395 private function setSessionDataForUser( $user, $remember = null ) {
2396 $session = $this->request
->getSession();
2397 $delay = $session->delaySave();
2399 $session->resetId();
2400 $session->resetAllTokens();
2401 if ( $session->canSetUser() ) {
2402 $session->setUser( $user );
2404 if ( $remember !== null ) {
2405 $session->setRememberUser( $remember );
2407 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2408 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2409 $session->persist();
2411 \Wikimedia\ScopedCallback
::consume( $delay );
2413 \Hooks
::run( 'UserLoggedIn', [ $user ] );
2418 * @param bool $useContextLang Use 'uselang' to set the user's language
2420 private function setDefaultUserOptions( User
$user, $useContextLang ) {
2423 $contLang = MediaWikiServices
::getInstance()->getContentLanguage();
2425 $lang = $useContextLang ? \RequestContext
::getMain()->getLanguage() : $contLang;
2426 $user->setOption( 'language', $lang->getPreferredVariant() );
2428 if ( $contLang->hasVariants() ) {
2429 $user->setOption( 'variant', $contLang->getPreferredVariant() );
2434 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2435 * @param string $method
2436 * @param array $args
2438 private function callMethodOnProviders( $which, $method, array $args ) {
2441 $providers +
= $this->getPreAuthenticationProviders();
2444 $providers +
= $this->getPrimaryAuthenticationProviders();
2447 $providers +
= $this->getSecondaryAuthenticationProviders();
2449 foreach ( $providers as $provider ) {
2450 $provider->$method( ...$args );
2455 * Reset the internal caching for unit testing
2456 * @protected Unit tests only
2458 public static function resetCache() {
2459 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2460 // @codeCoverageIgnoreStart
2461 throw new \
MWException( __METHOD__
. ' may only be called from unit tests!' );
2462 // @codeCoverageIgnoreEnd
2465 self
::$instance = null;
2473 * For really cool vim folding this needs to be at the end:
2474 * vim: foldmarker=@{,@} foldmethod=marker