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()).
58 * If you are writing code that is not associated with such a provider and needs to create accounts
59 * programmatically for real users, you should rethink your architecture. There is no good way to
60 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
61 * cannot provide any means for users to access the accounts it would create.
63 * The two main control flows when using this class are as follows:
64 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
65 * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
66 * exposing a form specification via the API, so that the client can build it), and pass them to
67 * the appropriate begin* method. That will return either a success/failure response, or more
68 * requests to fill (either by building a form or by redirecting the user to some external
69 * provider which will send the data back), in which case they need to be submitted to the
70 * appropriate continue* method and that step has to be repeated until the response is a success
71 * or failure response. AuthManager will use the session to maintain internal state during the
73 * * Code doing an authentication data change will call getAuthenticationRequests(), select
74 * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
75 * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
76 * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
81 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
83 class AuthManager
implements LoggerAwareInterface
{
84 /** Log in with an existing (not necessarily local) user */
85 const ACTION_LOGIN
= 'login';
86 /** Continue a login process that was interrupted by the need for user input or communication
87 * with an external provider */
88 const ACTION_LOGIN_CONTINUE
= 'login-continue';
89 /** Create a new user */
90 const ACTION_CREATE
= 'create';
91 /** Continue a user creation process that was interrupted by the need for user input or
92 * communication with an external provider */
93 const ACTION_CREATE_CONTINUE
= 'create-continue';
94 /** Link an existing user to a third-party account */
95 const ACTION_LINK
= 'link';
96 /** Continue a user linking process that was interrupted by the need for user input or
97 * communication with an external provider */
98 const ACTION_LINK_CONTINUE
= 'link-continue';
99 /** Change a user's credentials */
100 const ACTION_CHANGE
= 'change';
101 /** Remove a user's credentials */
102 const ACTION_REMOVE
= 'remove';
103 /** Like ACTION_REMOVE but for linking providers only */
104 const ACTION_UNLINK
= 'unlink';
106 /** Security-sensitive operations are ok. */
108 /** Security-sensitive operations should re-authenticate. */
109 const SEC_REAUTH
= 'reauth';
110 /** Security-sensitive should not be performed. */
111 const SEC_FAIL
= 'fail';
113 /** Auto-creation is due to SessionManager */
114 const AUTOCREATE_SOURCE_SESSION
= \MediaWiki\Session\SessionManager
::class;
116 /** @var AuthManager|null */
117 private static $instance = null;
119 /** @var WebRequest */
125 /** @var LoggerInterface */
128 /** @var AuthenticationProvider[] */
129 private $allAuthenticationProviders = [];
131 /** @var PreAuthenticationProvider[] */
132 private $preAuthenticationProviders = null;
134 /** @var PrimaryAuthenticationProvider[] */
135 private $primaryAuthenticationProviders = null;
137 /** @var SecondaryAuthenticationProvider[] */
138 private $secondaryAuthenticationProviders = null;
140 /** @var CreatedAccountAuthenticationRequest[] */
141 private $createdAccountAuthenticationRequests = [];
144 * Get the global AuthManager
145 * @return AuthManager
147 public static function singleton() {
148 if ( self
::$instance === null ) {
149 self
::$instance = new self(
150 \RequestContext
::getMain()->getRequest(),
151 MediaWikiServices
::getInstance()->getMainConfig()
154 return self
::$instance;
158 * @param WebRequest $request
159 * @param Config $config
161 public function __construct( WebRequest
$request, Config
$config ) {
162 $this->request
= $request;
163 $this->config
= $config;
164 $this->setLogger( \MediaWiki\Logger\LoggerFactory
::getInstance( 'authentication' ) );
168 * @param LoggerInterface $logger
170 public function setLogger( LoggerInterface
$logger ) {
171 $this->logger
= $logger;
177 public function getRequest() {
178 return $this->request
;
182 * Force certain PrimaryAuthenticationProviders
183 * @deprecated For backwards compatibility only
184 * @param PrimaryAuthenticationProvider[] $providers
187 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
188 $this->logger
->warning( "Overriding AuthManager primary authn because $why" );
190 if ( $this->primaryAuthenticationProviders
!== null ) {
191 $this->logger
->warning(
192 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
195 $this->allAuthenticationProviders
= array_diff_key(
196 $this->allAuthenticationProviders
,
197 $this->primaryAuthenticationProviders
199 $session = $this->request
->getSession();
200 $session->remove( 'AuthManager::authnState' );
201 $session->remove( 'AuthManager::accountCreationState' );
202 $session->remove( 'AuthManager::accountLinkState' );
203 $this->createdAccountAuthenticationRequests
= [];
206 $this->primaryAuthenticationProviders
= [];
207 foreach ( $providers as $provider ) {
208 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
209 throw new \
RuntimeException(
210 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
211 get_class( $provider )
214 $provider->setLogger( $this->logger
);
215 $provider->setManager( $this );
216 $provider->setConfig( $this->config
);
217 $id = $provider->getUniqueId();
218 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
219 throw new \
RuntimeException(
220 "Duplicate specifications for id $id (classes " .
221 get_class( $provider ) . ' and ' .
222 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
225 $this->allAuthenticationProviders
[$id] = $provider;
226 $this->primaryAuthenticationProviders
[$id] = $provider;
231 * Call a legacy AuthPlugin method, if necessary
232 * @codeCoverageIgnore
233 * @deprecated For backwards compatibility only, should be avoided in new code
234 * @param string $method AuthPlugin method to call
235 * @param array $params Parameters to pass
236 * @param mixed $return Return value if AuthPlugin wasn't called
237 * @return mixed Return value from the AuthPlugin method, or $return
239 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
242 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin
) {
243 return call_user_func_array( [ $wgAuth, $method ], $params );
250 * @name Authentication
255 * Indicate whether user authentication is possible
257 * It may not be if the session is provided by something like OAuth
258 * for which each individual request includes authentication data.
262 public function canAuthenticateNow() {
263 return $this->request
->getSession()->canSetUser();
267 * Start an authentication flow
269 * In addition to the AuthenticationRequests returned by
270 * $this->getAuthenticationRequests(), a client might include a
271 * CreateFromLoginAuthenticationRequest from a previous login attempt to
274 * Instead of the AuthenticationRequests returned by
275 * $this->getAuthenticationRequests(), a client might pass a
276 * CreatedAccountAuthenticationRequest from an account creation that just
277 * succeeded to log in to the just-created account.
279 * @param AuthenticationRequest[] $reqs
280 * @param string $returnToUrl Url that REDIRECT responses should eventually
282 * @return AuthenticationResponse See self::continueAuthentication()
284 public function beginAuthentication( array $reqs, $returnToUrl ) {
285 $session = $this->request
->getSession();
286 if ( !$session->canSetUser() ) {
287 // Caller should have called canAuthenticateNow()
288 $session->remove( 'AuthManager::authnState' );
289 throw new \
LogicException( 'Authentication is not possible now' );
292 $guessUserName = null;
293 foreach ( $reqs as $req ) {
294 $req->returnToUrl
= $returnToUrl;
295 // @codeCoverageIgnoreStart
296 if ( $req->username
!== null && $req->username
!== '' ) {
297 if ( $guessUserName === null ) {
298 $guessUserName = $req->username
;
299 } elseif ( $guessUserName !== $req->username
) {
300 $guessUserName = null;
304 // @codeCoverageIgnoreEnd
307 // Check for special-case login of a just-created account
308 $req = AuthenticationRequest
::getRequestByClass(
309 $reqs, CreatedAccountAuthenticationRequest
::class
312 if ( !in_array( $req, $this->createdAccountAuthenticationRequests
, true ) ) {
313 throw new \
LogicException(
314 'CreatedAccountAuthenticationRequests are only valid on ' .
315 'the same AuthManager that created the account'
319 $user = User
::newFromName( $req->username
);
320 // @codeCoverageIgnoreStart
322 throw new \
UnexpectedValueException(
323 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
325 } elseif ( $user->getId() != $req->id
) {
326 throw new \
UnexpectedValueException(
327 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
330 // @codeCoverageIgnoreEnd
332 $this->logger
->info( 'Logging in {user} after account creation', [
333 'user' => $user->getName(),
335 $ret = AuthenticationResponse
::newPass( $user->getName() );
336 $this->setSessionDataForUser( $user );
337 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
338 $session->remove( 'AuthManager::authnState' );
339 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
343 $this->removeAuthenticationSessionData( null );
345 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
346 $status = $provider->testForAuthentication( $reqs );
347 if ( !$status->isGood() ) {
348 $this->logger
->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
349 $ret = AuthenticationResponse
::newFail(
350 Status
::wrap( $status )->getMessage()
352 $this->callMethodOnProviders( 7, 'postAuthentication',
353 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
355 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
362 'returnToUrl' => $returnToUrl,
363 'guessUserName' => $guessUserName,
365 'primaryResponse' => null,
368 'continueRequests' => [],
371 // Preserve state from a previous failed login
372 $req = AuthenticationRequest
::getRequestByClass(
373 $reqs, CreateFromLoginAuthenticationRequest
::class
376 $state['maybeLink'] = $req->maybeLink
;
379 $session = $this->request
->getSession();
380 $session->setSecret( 'AuthManager::authnState', $state );
383 return $this->continueAuthentication( $reqs );
387 * Continue an authentication flow
389 * Return values are interpreted as follows:
390 * - status FAIL: Authentication failed. If $response->createRequest is
391 * set, that may be passed to self::beginAuthentication() or to
392 * self::beginAccountCreation() to preserve state.
393 * - status REDIRECT: The client should be redirected to the contained URL,
394 * new AuthenticationRequests should be made (if any), then
395 * AuthManager::continueAuthentication() should be called.
396 * - status UI: The client should be presented with a user interface for
397 * the fields in the specified AuthenticationRequests, then new
398 * AuthenticationRequests should be made, then
399 * AuthManager::continueAuthentication() should be called.
400 * - status RESTART: The user logged in successfully with a third-party
401 * service, but the third-party credentials aren't attached to any local
402 * account. This could be treated as a UI or a FAIL.
403 * - status PASS: Authentication was successful.
405 * @param AuthenticationRequest[] $reqs
406 * @return AuthenticationResponse
408 public function continueAuthentication( array $reqs ) {
409 $session = $this->request
->getSession();
411 if ( !$session->canSetUser() ) {
412 // Caller should have called canAuthenticateNow()
413 // @codeCoverageIgnoreStart
414 throw new \
LogicException( 'Authentication is not possible now' );
415 // @codeCoverageIgnoreEnd
418 $state = $session->getSecret( 'AuthManager::authnState' );
419 if ( !is_array( $state ) ) {
420 return AuthenticationResponse
::newFail(
421 wfMessage( 'authmanager-authn-not-in-progress' )
424 $state['continueRequests'] = [];
426 $guessUserName = $state['guessUserName'];
428 foreach ( $reqs as $req ) {
429 $req->returnToUrl
= $state['returnToUrl'];
432 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
434 if ( $state['primary'] === null ) {
435 // We haven't picked a PrimaryAuthenticationProvider yet
436 // @codeCoverageIgnoreStart
437 $guessUserName = null;
438 foreach ( $reqs as $req ) {
439 if ( $req->username
!== null && $req->username
!== '' ) {
440 if ( $guessUserName === null ) {
441 $guessUserName = $req->username
;
442 } elseif ( $guessUserName !== $req->username
) {
443 $guessUserName = null;
448 $state['guessUserName'] = $guessUserName;
449 // @codeCoverageIgnoreEnd
450 $state['reqs'] = $reqs;
452 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
453 $res = $provider->beginPrimaryAuthentication( $reqs );
454 switch ( $res->status
) {
455 case AuthenticationResponse
::PASS
;
456 $state['primary'] = $id;
457 $state['primaryResponse'] = $res;
458 $this->logger
->debug( "Primary login with $id succeeded" );
460 case AuthenticationResponse
::FAIL
;
461 $this->logger
->debug( "Login failed in primary authentication by $id" );
462 if ( $res->createRequest ||
$state['maybeLink'] ) {
463 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
464 $res->createRequest
, $state['maybeLink']
467 $this->callMethodOnProviders( 7, 'postAuthentication',
468 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
470 $session->remove( 'AuthManager::authnState' );
471 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
473 case AuthenticationResponse
::ABSTAIN
;
476 case AuthenticationResponse
::REDIRECT
;
477 case AuthenticationResponse
::UI
;
478 $this->logger
->debug( "Primary login with $id returned $res->status" );
479 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
480 $state['primary'] = $id;
481 $state['continueRequests'] = $res->neededRequests
;
482 $session->setSecret( 'AuthManager::authnState', $state );
485 // @codeCoverageIgnoreStart
487 throw new \
DomainException(
488 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
490 // @codeCoverageIgnoreEnd
493 if ( $state['primary'] === null ) {
494 $this->logger
->debug( 'Login failed in primary authentication because no provider accepted' );
495 $ret = AuthenticationResponse
::newFail(
496 wfMessage( 'authmanager-authn-no-primary' )
498 $this->callMethodOnProviders( 7, 'postAuthentication',
499 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
501 $session->remove( 'AuthManager::authnState' );
504 } elseif ( $state['primaryResponse'] === null ) {
505 $provider = $this->getAuthenticationProvider( $state['primary'] );
506 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
507 // Configuration changed? Force them to start over.
508 // @codeCoverageIgnoreStart
509 $ret = AuthenticationResponse
::newFail(
510 wfMessage( 'authmanager-authn-not-in-progress' )
512 $this->callMethodOnProviders( 7, 'postAuthentication',
513 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
515 $session->remove( 'AuthManager::authnState' );
517 // @codeCoverageIgnoreEnd
519 $id = $provider->getUniqueId();
520 $res = $provider->continuePrimaryAuthentication( $reqs );
521 switch ( $res->status
) {
522 case AuthenticationResponse
::PASS
;
523 $state['primaryResponse'] = $res;
524 $this->logger
->debug( "Primary login with $id succeeded" );
526 case AuthenticationResponse
::FAIL
;
527 $this->logger
->debug( "Login failed in primary authentication by $id" );
528 if ( $res->createRequest ||
$state['maybeLink'] ) {
529 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
530 $res->createRequest
, $state['maybeLink']
533 $this->callMethodOnProviders( 7, 'postAuthentication',
534 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
536 $session->remove( 'AuthManager::authnState' );
537 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
539 case AuthenticationResponse
::REDIRECT
;
540 case AuthenticationResponse
::UI
;
541 $this->logger
->debug( "Primary login with $id returned $res->status" );
542 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
543 $state['continueRequests'] = $res->neededRequests
;
544 $session->setSecret( 'AuthManager::authnState', $state );
547 throw new \
DomainException(
548 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
553 $res = $state['primaryResponse'];
554 if ( $res->username
=== null ) {
555 $provider = $this->getAuthenticationProvider( $state['primary'] );
556 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
557 // Configuration changed? Force them to start over.
558 // @codeCoverageIgnoreStart
559 $ret = AuthenticationResponse
::newFail(
560 wfMessage( 'authmanager-authn-not-in-progress' )
562 $this->callMethodOnProviders( 7, 'postAuthentication',
563 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
565 $session->remove( 'AuthManager::authnState' );
567 // @codeCoverageIgnoreEnd
570 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
&&
572 // don't confuse the user with an incorrect message if linking is disabled
573 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider
::class )
575 $state['maybeLink'][$res->linkRequest
->getUniqueId()] = $res->linkRequest
;
576 $msg = 'authmanager-authn-no-local-user-link';
578 $msg = 'authmanager-authn-no-local-user';
580 $this->logger
->debug(
581 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
583 $ret = AuthenticationResponse
::newRestart( wfMessage( $msg ) );
584 $ret->neededRequests
= $this->getAuthenticationRequestsInternal(
587 $this->getPrimaryAuthenticationProviders() +
$this->getSecondaryAuthenticationProviders()
589 if ( $res->createRequest ||
$state['maybeLink'] ) {
590 $ret->createRequest
= new CreateFromLoginAuthenticationRequest(
591 $res->createRequest
, $state['maybeLink']
593 $ret->neededRequests
[] = $ret->createRequest
;
595 $this->fillRequests( $ret->neededRequests
, self
::ACTION_LOGIN
, null, true );
596 $session->setSecret( 'AuthManager::authnState', [
597 'reqs' => [], // Will be filled in later
599 'primaryResponse' => null,
601 'continueRequests' => $ret->neededRequests
,
606 // Step 2: Primary authentication succeeded, create the User object
607 // (and add the user locally if necessary)
609 $user = User
::newFromName( $res->username
, 'usable' );
611 $provider = $this->getAuthenticationProvider( $state['primary'] );
612 throw new \
DomainException(
613 get_class( $provider ) . " returned an invalid username: {$res->username}"
616 if ( $user->getId() === 0 ) {
617 // User doesn't exist locally. Create it.
618 $this->logger
->info( 'Auto-creating {user} on login', [
619 'user' => $user->getName(),
621 $status = $this->autoCreateUser( $user, $state['primary'], false );
622 if ( !$status->isGood() ) {
623 $ret = AuthenticationResponse
::newFail(
624 Status
::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
626 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
627 $session->remove( 'AuthManager::authnState' );
628 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
633 // Step 3: Iterate over all the secondary authentication providers.
635 $beginReqs = $state['reqs'];
637 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
638 if ( !isset( $state['secondary'][$id] ) ) {
639 // This provider isn't started yet, so we pass it the set
640 // of reqs from beginAuthentication instead of whatever
641 // might have been used by a previous provider in line.
642 $func = 'beginSecondaryAuthentication';
643 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
644 } elseif ( !$state['secondary'][$id] ) {
645 $func = 'continueSecondaryAuthentication';
646 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
650 switch ( $res->status
) {
651 case AuthenticationResponse
::PASS
;
652 $this->logger
->debug( "Secondary login with $id succeeded" );
654 case AuthenticationResponse
::ABSTAIN
;
655 $state['secondary'][$id] = true;
657 case AuthenticationResponse
::FAIL
;
658 $this->logger
->debug( "Login failed in secondary authentication by $id" );
659 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
660 $session->remove( 'AuthManager::authnState' );
661 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
663 case AuthenticationResponse
::REDIRECT
;
664 case AuthenticationResponse
::UI
;
665 $this->logger
->debug( "Secondary login with $id returned " . $res->status
);
666 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $user->getName() );
667 $state['secondary'][$id] = false;
668 $state['continueRequests'] = $res->neededRequests
;
669 $session->setSecret( 'AuthManager::authnState', $state );
672 // @codeCoverageIgnoreStart
674 throw new \
DomainException(
675 get_class( $provider ) . "::{$func}() returned $res->status"
677 // @codeCoverageIgnoreEnd
681 // Step 4: Authentication complete! Set the user in the session and
684 $this->logger
->info( 'Login for {user} succeeded from {clientip}', [
685 'user' => $user->getName(),
686 'clientip' => $this->request
->getIP(),
688 /** @var RememberMeAuthenticationRequest $req */
689 $req = AuthenticationRequest
::getRequestByClass(
690 $beginReqs, RememberMeAuthenticationRequest
::class
692 $this->setSessionDataForUser( $user, $req && $req->rememberMe
);
693 $ret = AuthenticationResponse
::newPass( $user->getName() );
694 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
695 $session->remove( 'AuthManager::authnState' );
696 $this->removeAuthenticationSessionData( null );
697 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
699 } catch ( \Exception
$ex ) {
700 $session->remove( 'AuthManager::authnState' );
706 * Whether security-sensitive operations should proceed.
708 * A "security-sensitive operation" is something like a password or email
709 * change, that would normally have a "reenter your password to confirm"
710 * box if we only supported password-based authentication.
712 * @param string $operation Operation being checked. This should be a
713 * message-key-like string such as 'change-password' or 'change-email'.
714 * @return string One of the SEC_* constants.
716 public function securitySensitiveOperationStatus( $operation ) {
717 $status = self
::SEC_OK
;
719 $this->logger
->debug( __METHOD__
. ": Checking $operation" );
721 $session = $this->request
->getSession();
722 $aId = $session->getUser()->getId();
724 // User isn't authenticated. DWIM?
725 $status = $this->canAuthenticateNow() ? self
::SEC_REAUTH
: self
::SEC_FAIL
;
726 $this->logger
->info( __METHOD__
. ": Not logged in! $operation is $status" );
730 if ( $session->canSetUser() ) {
731 $id = $session->get( 'AuthManager:lastAuthId' );
732 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
733 if ( $id !== $aId ||
$last === null ) {
734 $timeSinceLogin = PHP_INT_MAX
; // Forever ago
736 $timeSinceLogin = max( 0, time() - $last );
739 $thresholds = $this->config
->get( 'ReauthenticateTime' );
740 if ( isset( $thresholds[$operation] ) ) {
741 $threshold = $thresholds[$operation];
742 } elseif ( isset( $thresholds['default'] ) ) {
743 $threshold = $thresholds['default'];
745 throw new \
UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
748 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
749 $status = self
::SEC_REAUTH
;
752 $timeSinceLogin = -1;
754 $pass = $this->config
->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
755 if ( isset( $pass[$operation] ) ) {
756 $status = $pass[$operation] ? self
::SEC_OK
: self
::SEC_FAIL
;
757 } elseif ( isset( $pass['default'] ) ) {
758 $status = $pass['default'] ? self
::SEC_OK
: self
::SEC_FAIL
;
760 throw new \
UnexpectedValueException(
761 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
766 \Hooks
::run( 'SecuritySensitiveOperationStatus', [
767 &$status, $operation, $session, $timeSinceLogin
770 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
771 if ( !$this->canAuthenticateNow() && $status === self
::SEC_REAUTH
) {
772 $status = self
::SEC_FAIL
;
775 $this->logger
->info( __METHOD__
. ": $operation is $status" );
781 * Determine whether a username can authenticate
783 * This is mainly for internal purposes and only takes authentication data into account,
784 * not things like blocks that can change without the authentication system being aware.
786 * @param string $username MediaWiki username
789 public function userCanAuthenticate( $username ) {
790 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
791 if ( $provider->testUserCanAuthenticate( $username ) ) {
799 * Provide normalized versions of the username for security checks
801 * Since different providers can normalize the input in different ways,
802 * this returns an array of all the different ways the name might be
803 * normalized for authentication.
805 * The returned strings should not be revealed to the user, as that might
806 * leak private information (e.g. an email address might be normalized to a
809 * @param string $username
812 public function normalizeUsername( $username ) {
814 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
815 $normalized = $provider->providerNormalizeUsername( $username );
816 if ( $normalized !== null ) {
817 $ret[$normalized] = true;
820 return array_keys( $ret );
826 * @name Authentication data changing
831 * Revoke any authentication credentials for a user
833 * After this, the user should no longer be able to log in.
835 * @param string $username
837 public function revokeAccessForUser( $username ) {
838 $this->logger
->info( 'Revoking access for {user}', [
841 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
845 * Validate a change of authentication data (e.g. passwords)
846 * @param AuthenticationRequest $req
847 * @param bool $checkData If false, $req hasn't been loaded from the
848 * submission so checks on user-submitted fields should be skipped. $req->username is
849 * considered user-submitted for this purpose, even if it cannot be changed via
850 * $req->loadFromSubmission.
853 public function allowsAuthenticationDataChange( AuthenticationRequest
$req, $checkData = true ) {
855 $providers = $this->getPrimaryAuthenticationProviders() +
856 $this->getSecondaryAuthenticationProviders();
857 foreach ( $providers as $provider ) {
858 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
859 if ( !$status->isGood() ) {
860 return Status
::wrap( $status );
862 $any = $any ||
$status->value
!== 'ignored';
865 $status = Status
::newGood( 'ignored' );
866 $status->warning( 'authmanager-change-not-supported' );
869 return Status
::newGood();
873 * Change authentication data (e.g. passwords)
875 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
876 * result in a successful login in the future.
878 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
879 * no longer result in a successful login.
881 * This method should only be called if allowsAuthenticationDataChange( $req, true )
884 * @param AuthenticationRequest $req
885 * @param bool $isAddition Set true if this represents an addition of
886 * credentials rather than a change. The main difference is that additions
887 * should not invalidate BotPasswords. If you're not sure, leave it false.
889 public function changeAuthenticationData( AuthenticationRequest
$req, $isAddition = false ) {
890 $this->logger
->info( 'Changing authentication data for {user} class {what}', [
891 'user' => is_string( $req->username
) ?
$req->username
: '<no name>',
892 'what' => get_class( $req ),
895 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
897 // When the main account's authentication data is changed, invalidate
898 // all BotPasswords too.
899 if ( !$isAddition ) {
900 \BotPassword
::invalidateAllPasswordsForUser( $req->username
);
907 * @name Account creation
912 * Determine whether accounts can be created
915 public function canCreateAccounts() {
916 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
917 switch ( $provider->accountCreationType() ) {
918 case PrimaryAuthenticationProvider
::TYPE_CREATE
:
919 case PrimaryAuthenticationProvider
::TYPE_LINK
:
927 * Determine whether a particular account can be created
928 * @param string $username MediaWiki username
929 * @param array $options
930 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
931 * - creating: (bool) For internal use only. Never specify this.
934 public function canCreateAccount( $username, $options = [] ) {
936 if ( is_int( $options ) ) {
937 $options = [ 'flags' => $options ];
940 'flags' => User
::READ_NORMAL
,
943 $flags = $options['flags'];
945 if ( !$this->canCreateAccounts() ) {
946 return Status
::newFatal( 'authmanager-create-disabled' );
949 if ( $this->userExists( $username, $flags ) ) {
950 return Status
::newFatal( 'userexists' );
953 $user = User
::newFromName( $username, 'creatable' );
954 if ( !is_object( $user ) ) {
955 return Status
::newFatal( 'noname' );
957 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
958 if ( $user->getId() !== 0 ) {
959 return Status
::newFatal( 'userexists' );
963 // Denied by providers?
964 $providers = $this->getPreAuthenticationProviders() +
965 $this->getPrimaryAuthenticationProviders() +
966 $this->getSecondaryAuthenticationProviders();
967 foreach ( $providers as $provider ) {
968 $status = $provider->testUserForCreation( $user, false, $options );
969 if ( !$status->isGood() ) {
970 return Status
::wrap( $status );
974 return Status
::newGood();
978 * Basic permissions checks on whether a user can create accounts
979 * @param User $creator User doing the account creation
982 public function checkAccountCreatePermissions( User
$creator ) {
983 // Wiki is read-only?
984 if ( wfReadOnly() ) {
985 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
988 // This is awful, this permission check really shouldn't go through Title.
989 $permErrors = \SpecialPage
::getTitleFor( 'CreateAccount' )
990 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
992 $status = Status
::newGood();
993 foreach ( $permErrors as $args ) {
994 call_user_func_array( [ $status, 'fatal' ], $args );
999 $block = $creator->isBlockedFromCreateAccount();
1002 $block->getTarget(),
1003 $block->mReason ?
: wfMessage( 'blockednoreason' )->text(),
1007 if ( $block->getType() === \Block
::TYPE_RANGE
) {
1008 $errorMessage = 'cantcreateaccount-range-text';
1009 $errorParams[] = $this->getRequest()->getIP();
1011 $errorMessage = 'cantcreateaccount-text';
1014 return Status
::newFatal( wfMessage( $errorMessage, $errorParams ) );
1017 $ip = $this->getRequest()->getIP();
1018 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1019 return Status
::newFatal( 'sorbs_create_account_reason' );
1022 return Status
::newGood();
1026 * Start an account creation flow
1028 * In addition to the AuthenticationRequests returned by
1029 * $this->getAuthenticationRequests(), a client might include a
1030 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1032 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1034 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1035 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1036 * username set, that username must be used for all other requests.
1038 * @param User $creator User doing the account creation
1039 * @param AuthenticationRequest[] $reqs
1040 * @param string $returnToUrl Url that REDIRECT responses should eventually
1042 * @return AuthenticationResponse
1044 public function beginAccountCreation( User
$creator, array $reqs, $returnToUrl ) {
1045 $session = $this->request
->getSession();
1046 if ( !$this->canCreateAccounts() ) {
1047 // Caller should have called canCreateAccounts()
1048 $session->remove( 'AuthManager::accountCreationState' );
1049 throw new \
LogicException( 'Account creation is not possible' );
1053 $username = AuthenticationRequest
::getUsernameFromRequests( $reqs );
1054 } catch ( \UnexpectedValueException
$ex ) {
1057 if ( $username === null ) {
1058 $this->logger
->debug( __METHOD__
. ': No username provided' );
1059 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1062 // Permissions check
1063 $status = $this->checkAccountCreatePermissions( $creator );
1064 if ( !$status->isGood() ) {
1065 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1066 'user' => $username,
1067 'creator' => $creator->getName(),
1068 'reason' => $status->getWikiText( null, null, 'en' )
1070 return AuthenticationResponse
::newFail( $status->getMessage() );
1073 $status = $this->canCreateAccount(
1074 $username, [ 'flags' => User
::READ_LOCKING
, 'creating' => true ]
1076 if ( !$status->isGood() ) {
1077 $this->logger
->debug( __METHOD__
. ': {user} cannot be created: {reason}', [
1078 'user' => $username,
1079 'creator' => $creator->getName(),
1080 'reason' => $status->getWikiText( null, null, 'en' )
1082 return AuthenticationResponse
::newFail( $status->getMessage() );
1085 $user = User
::newFromName( $username, 'creatable' );
1086 foreach ( $reqs as $req ) {
1087 $req->username
= $username;
1088 $req->returnToUrl
= $returnToUrl;
1089 if ( $req instanceof UserDataAuthenticationRequest
) {
1090 $status = $req->populateUser( $user );
1091 if ( !$status->isGood() ) {
1092 $status = Status
::wrap( $status );
1093 $session->remove( 'AuthManager::accountCreationState' );
1094 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1095 'user' => $user->getName(),
1096 'creator' => $creator->getName(),
1097 'reason' => $status->getWikiText( null, null, 'en' ),
1099 return AuthenticationResponse
::newFail( $status->getMessage() );
1104 $this->removeAuthenticationSessionData( null );
1107 'username' => $username,
1109 'creatorid' => $creator->getId(),
1110 'creatorname' => $creator->getName(),
1112 'returnToUrl' => $returnToUrl,
1114 'primaryResponse' => null,
1116 'continueRequests' => [],
1118 'ranPreTests' => false,
1121 // Special case: converting a login to an account creation
1122 $req = AuthenticationRequest
::getRequestByClass(
1123 $reqs, CreateFromLoginAuthenticationRequest
::class
1126 $state['maybeLink'] = $req->maybeLink
;
1128 if ( $req->createRequest
) {
1129 $reqs[] = $req->createRequest
;
1130 $state['reqs'][] = $req->createRequest
;
1134 $session->setSecret( 'AuthManager::accountCreationState', $state );
1135 $session->persist();
1137 return $this->continueAccountCreation( $reqs );
1141 * Continue an account creation flow
1142 * @param AuthenticationRequest[] $reqs
1143 * @return AuthenticationResponse
1145 public function continueAccountCreation( array $reqs ) {
1146 $session = $this->request
->getSession();
1148 if ( !$this->canCreateAccounts() ) {
1149 // Caller should have called canCreateAccounts()
1150 $session->remove( 'AuthManager::accountCreationState' );
1151 throw new \
LogicException( 'Account creation is not possible' );
1154 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1155 if ( !is_array( $state ) ) {
1156 return AuthenticationResponse
::newFail(
1157 wfMessage( 'authmanager-create-not-in-progress' )
1160 $state['continueRequests'] = [];
1162 // Step 0: Prepare and validate the input
1164 $user = User
::newFromName( $state['username'], 'creatable' );
1165 if ( !is_object( $user ) ) {
1166 $session->remove( 'AuthManager::accountCreationState' );
1167 $this->logger
->debug( __METHOD__
. ': Invalid username', [
1168 'user' => $state['username'],
1170 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1173 if ( $state['creatorid'] ) {
1174 $creator = User
::newFromId( $state['creatorid'] );
1176 $creator = new User
;
1177 $creator->setName( $state['creatorname'] );
1180 // Avoid account creation races on double submissions
1181 $cache = \ObjectCache
::getLocalClusterInstance();
1182 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1184 // Don't clear AuthManager::accountCreationState for this code
1185 // path because the process that won the race owns it.
1186 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1187 'user' => $user->getName(),
1188 'creator' => $creator->getName(),
1190 return AuthenticationResponse
::newFail( wfMessage( 'usernameinprogress' ) );
1193 // Permissions check
1194 $status = $this->checkAccountCreatePermissions( $creator );
1195 if ( !$status->isGood() ) {
1196 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1197 'user' => $user->getName(),
1198 'creator' => $creator->getName(),
1199 'reason' => $status->getWikiText( null, null, 'en' )
1201 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1202 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1203 $session->remove( 'AuthManager::accountCreationState' );
1207 // Load from master for existence check
1208 $user->load( User
::READ_LOCKING
);
1210 if ( $state['userid'] === 0 ) {
1211 if ( $user->getId() != 0 ) {
1212 $this->logger
->debug( __METHOD__
. ': User exists locally', [
1213 'user' => $user->getName(),
1214 'creator' => $creator->getName(),
1216 $ret = AuthenticationResponse
::newFail( wfMessage( 'userexists' ) );
1217 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1218 $session->remove( 'AuthManager::accountCreationState' );
1222 if ( $user->getId() == 0 ) {
1223 $this->logger
->debug( __METHOD__
. ': User does not exist locally when it should', [
1224 'user' => $user->getName(),
1225 'creator' => $creator->getName(),
1226 'expected_id' => $state['userid'],
1228 throw new \
UnexpectedValueException(
1229 "User \"{$state['username']}\" should exist now, but doesn't!"
1232 if ( $user->getId() != $state['userid'] ) {
1233 $this->logger
->debug( __METHOD__
. ': User ID/name mismatch', [
1234 'user' => $user->getName(),
1235 'creator' => $creator->getName(),
1236 'expected_id' => $state['userid'],
1237 'actual_id' => $user->getId(),
1239 throw new \
UnexpectedValueException(
1240 "User \"{$state['username']}\" exists, but " .
1241 "ID {$user->getId()} != {$state['userid']}!"
1245 foreach ( $state['reqs'] as $req ) {
1246 if ( $req instanceof UserDataAuthenticationRequest
) {
1247 $status = $req->populateUser( $user );
1248 if ( !$status->isGood() ) {
1249 // This should never happen...
1250 $status = Status
::wrap( $status );
1251 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1252 'user' => $user->getName(),
1253 'creator' => $creator->getName(),
1254 'reason' => $status->getWikiText( null, null, 'en' ),
1256 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1257 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1258 $session->remove( 'AuthManager::accountCreationState' );
1264 foreach ( $reqs as $req ) {
1265 $req->returnToUrl
= $state['returnToUrl'];
1266 $req->username
= $state['username'];
1269 // Run pre-creation tests, if we haven't already
1270 if ( !$state['ranPreTests'] ) {
1271 $providers = $this->getPreAuthenticationProviders() +
1272 $this->getPrimaryAuthenticationProviders() +
1273 $this->getSecondaryAuthenticationProviders();
1274 foreach ( $providers as $id => $provider ) {
1275 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1276 if ( !$status->isGood() ) {
1277 $this->logger
->debug( __METHOD__
. ": Fail in pre-authentication by $id", [
1278 'user' => $user->getName(),
1279 'creator' => $creator->getName(),
1281 $ret = AuthenticationResponse
::newFail(
1282 Status
::wrap( $status )->getMessage()
1284 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1285 $session->remove( 'AuthManager::accountCreationState' );
1290 $state['ranPreTests'] = true;
1293 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1295 if ( $state['primary'] === null ) {
1296 // We haven't picked a PrimaryAuthenticationProvider yet
1297 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1298 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_NONE
) {
1301 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1302 switch ( $res->status
) {
1303 case AuthenticationResponse
::PASS
;
1304 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1305 'user' => $user->getName(),
1306 'creator' => $creator->getName(),
1308 $state['primary'] = $id;
1309 $state['primaryResponse'] = $res;
1311 case AuthenticationResponse
::FAIL
;
1312 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1313 'user' => $user->getName(),
1314 'creator' => $creator->getName(),
1316 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1317 $session->remove( 'AuthManager::accountCreationState' );
1319 case AuthenticationResponse
::ABSTAIN
;
1322 case AuthenticationResponse
::REDIRECT
;
1323 case AuthenticationResponse
::UI
;
1324 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1325 'user' => $user->getName(),
1326 'creator' => $creator->getName(),
1328 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1329 $state['primary'] = $id;
1330 $state['continueRequests'] = $res->neededRequests
;
1331 $session->setSecret( 'AuthManager::accountCreationState', $state );
1334 // @codeCoverageIgnoreStart
1336 throw new \
DomainException(
1337 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1339 // @codeCoverageIgnoreEnd
1342 if ( $state['primary'] === null ) {
1343 $this->logger
->debug( __METHOD__
. ': Primary creation failed because no provider accepted', [
1344 'user' => $user->getName(),
1345 'creator' => $creator->getName(),
1347 $ret = AuthenticationResponse
::newFail(
1348 wfMessage( 'authmanager-create-no-primary' )
1350 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1351 $session->remove( 'AuthManager::accountCreationState' );
1354 } elseif ( $state['primaryResponse'] === null ) {
1355 $provider = $this->getAuthenticationProvider( $state['primary'] );
1356 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1357 // Configuration changed? Force them to start over.
1358 // @codeCoverageIgnoreStart
1359 $ret = AuthenticationResponse
::newFail(
1360 wfMessage( 'authmanager-create-not-in-progress' )
1362 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1363 $session->remove( 'AuthManager::accountCreationState' );
1365 // @codeCoverageIgnoreEnd
1367 $id = $provider->getUniqueId();
1368 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1369 switch ( $res->status
) {
1370 case AuthenticationResponse
::PASS
;
1371 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1372 'user' => $user->getName(),
1373 'creator' => $creator->getName(),
1375 $state['primaryResponse'] = $res;
1377 case AuthenticationResponse
::FAIL
;
1378 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1379 'user' => $user->getName(),
1380 'creator' => $creator->getName(),
1382 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1383 $session->remove( 'AuthManager::accountCreationState' );
1385 case AuthenticationResponse
::REDIRECT
;
1386 case AuthenticationResponse
::UI
;
1387 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1388 'user' => $user->getName(),
1389 'creator' => $creator->getName(),
1391 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1392 $state['continueRequests'] = $res->neededRequests
;
1393 $session->setSecret( 'AuthManager::accountCreationState', $state );
1396 throw new \
DomainException(
1397 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1402 // Step 2: Primary authentication succeeded, create the User object
1403 // and add the user locally.
1405 if ( $state['userid'] === 0 ) {
1406 $this->logger
->info( 'Creating user {user} during account creation', [
1407 'user' => $user->getName(),
1408 'creator' => $creator->getName(),
1410 $status = $user->addToDatabase();
1411 if ( !$status->isOK() ) {
1412 // @codeCoverageIgnoreStart
1413 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1414 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1415 $session->remove( 'AuthManager::accountCreationState' );
1417 // @codeCoverageIgnoreEnd
1419 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1420 \Hooks
::run( 'LocalUserCreated', [ $user, false ] );
1421 $user->saveSettings();
1422 $state['userid'] = $user->getId();
1424 // Update user count
1425 \DeferredUpdates
::addUpdate( \SiteStatsUpdate
::factory( [ 'users' => 1 ] ) );
1427 // Watch user's userpage and talk page
1428 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1430 // Inform the provider
1431 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1434 if ( $this->config
->get( 'NewUserLog' ) ) {
1435 $isAnon = $creator->isAnon();
1436 $logEntry = new \
ManualLogEntry(
1438 $logSubtype ?
: ( $isAnon ?
'create' : 'create2' )
1440 $logEntry->setPerformer( $isAnon ?
$user : $creator );
1441 $logEntry->setTarget( $user->getUserPage() );
1442 /** @var CreationReasonAuthenticationRequest $req */
1443 $req = AuthenticationRequest
::getRequestByClass(
1444 $state['reqs'], CreationReasonAuthenticationRequest
::class
1446 $logEntry->setComment( $req ?
$req->reason
: '' );
1447 $logEntry->setParameters( [
1448 '4::userid' => $user->getId(),
1450 $logid = $logEntry->insert();
1451 $logEntry->publish( $logid );
1455 // Step 3: Iterate over all the secondary authentication providers.
1457 $beginReqs = $state['reqs'];
1459 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1460 if ( !isset( $state['secondary'][$id] ) ) {
1461 // This provider isn't started yet, so we pass it the set
1462 // of reqs from beginAuthentication instead of whatever
1463 // might have been used by a previous provider in line.
1464 $func = 'beginSecondaryAccountCreation';
1465 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1466 } elseif ( !$state['secondary'][$id] ) {
1467 $func = 'continueSecondaryAccountCreation';
1468 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1472 switch ( $res->status
) {
1473 case AuthenticationResponse
::PASS
;
1474 $this->logger
->debug( __METHOD__
. ": Secondary creation passed by $id", [
1475 'user' => $user->getName(),
1476 'creator' => $creator->getName(),
1479 case AuthenticationResponse
::ABSTAIN
;
1480 $state['secondary'][$id] = true;
1482 case AuthenticationResponse
::REDIRECT
;
1483 case AuthenticationResponse
::UI
;
1484 $this->logger
->debug( __METHOD__
. ": Secondary creation $res->status by $id", [
1485 'user' => $user->getName(),
1486 'creator' => $creator->getName(),
1488 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1489 $state['secondary'][$id] = false;
1490 $state['continueRequests'] = $res->neededRequests
;
1491 $session->setSecret( 'AuthManager::accountCreationState', $state );
1493 case AuthenticationResponse
::FAIL
;
1494 throw new \
DomainException(
1495 get_class( $provider ) . "::{$func}() returned $res->status." .
1496 ' Secondary providers are not allowed to fail account creation, that' .
1497 ' should have been done via testForAccountCreation().'
1499 // @codeCoverageIgnoreStart
1501 throw new \
DomainException(
1502 get_class( $provider ) . "::{$func}() returned $res->status"
1504 // @codeCoverageIgnoreEnd
1508 $id = $user->getId();
1509 $name = $user->getName();
1510 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1511 $ret = AuthenticationResponse
::newPass( $name );
1512 $ret->loginRequest
= $req;
1513 $this->createdAccountAuthenticationRequests
[] = $req;
1515 $this->logger
->info( __METHOD__
. ': Account creation succeeded for {user}', [
1516 'user' => $user->getName(),
1517 'creator' => $creator->getName(),
1520 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1521 $session->remove( 'AuthManager::accountCreationState' );
1522 $this->removeAuthenticationSessionData( null );
1524 } catch ( \Exception
$ex ) {
1525 $session->remove( 'AuthManager::accountCreationState' );
1531 * Auto-create an account, and log into that account
1533 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1534 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1535 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1536 * the username of a non-existing user from provideSessionInfo(). Calling this method
1537 * explicitly (e.g. from a maintenance script) is also fine.
1539 * @param User $user User to auto-create
1540 * @param string $source What caused the auto-creation? This must be the ID
1541 * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1542 * @param bool $login Whether to also log the user in
1543 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1545 public function autoCreateUser( User
$user, $source, $login = true ) {
1546 if ( $source !== self
::AUTOCREATE_SOURCE_SESSION
&&
1547 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1549 throw new \
InvalidArgumentException( "Unknown auto-creation source: $source" );
1552 $username = $user->getName();
1554 // Try the local user from the replica DB
1555 $localId = User
::idFromName( $username );
1556 $flags = User
::READ_NORMAL
;
1558 // Fetch the user ID from the master, so that we don't try to create the user
1559 // when they already exist, due to replication lag
1560 // @codeCoverageIgnoreStart
1563 MediaWikiServices
::getInstance()->getDBLoadBalancer()->getReaderIndex() != 0
1565 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1566 $flags = User
::READ_LATEST
;
1568 // @codeCoverageIgnoreEnd
1571 $this->logger
->debug( __METHOD__
. ': {username} already exists locally', [
1572 'username' => $username,
1574 $user->setId( $localId );
1575 $user->loadFromId( $flags );
1577 $this->setSessionDataForUser( $user );
1579 $status = Status
::newGood();
1580 $status->warning( 'userexists' );
1584 // Wiki is read-only?
1585 if ( wfReadOnly() ) {
1586 $this->logger
->debug( __METHOD__
. ': denied by wfReadOnly(): {reason}', [
1587 'username' => $username,
1588 'reason' => wfReadOnlyReason(),
1591 $user->loadFromId();
1592 return Status
::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1595 // Check the session, if we tried to create this user already there's
1596 // no point in retrying.
1597 $session = $this->request
->getSession();
1598 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1599 $this->logger
->debug( __METHOD__
. ': blacklisted in session {sessionid}', [
1600 'username' => $username,
1601 'sessionid' => $session->getId(),
1604 $user->loadFromId();
1605 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1606 if ( $reason instanceof StatusValue
) {
1607 return Status
::wrap( $reason );
1609 return Status
::newFatal( $reason );
1613 // Is the username creatable?
1614 if ( !User
::isCreatableName( $username ) ) {
1615 $this->logger
->debug( __METHOD__
. ': name "{username}" is not creatable', [
1616 'username' => $username,
1618 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1620 $user->loadFromId();
1621 return Status
::newFatal( 'noname' );
1624 // Is the IP user able to create accounts?
1626 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1627 $this->logger
->debug( __METHOD__
. ': IP lacks the ability to create or autocreate accounts', [
1628 'username' => $username,
1629 'ip' => $anon->getName(),
1631 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1632 $session->persist();
1634 $user->loadFromId();
1635 return Status
::newFatal( 'authmanager-autocreate-noperm' );
1638 // Avoid account creation races on double submissions
1639 $cache = \ObjectCache
::getLocalClusterInstance();
1640 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1642 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1643 'user' => $username,
1646 $user->loadFromId();
1647 return Status
::newFatal( 'usernameinprogress' );
1650 // Denied by providers?
1652 'flags' => User
::READ_LATEST
,
1655 $providers = $this->getPreAuthenticationProviders() +
1656 $this->getPrimaryAuthenticationProviders() +
1657 $this->getSecondaryAuthenticationProviders();
1658 foreach ( $providers as $provider ) {
1659 $status = $provider->testUserForCreation( $user, $source, $options );
1660 if ( !$status->isGood() ) {
1661 $ret = Status
::wrap( $status );
1662 $this->logger
->debug( __METHOD__
. ': Provider denied creation of {username}: {reason}', [
1663 'username' => $username,
1664 'reason' => $ret->getWikiText( null, null, 'en' ),
1666 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1668 $user->loadFromId();
1673 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1674 if ( $cache->get( $backoffKey ) ) {
1675 $this->logger
->debug( __METHOD__
. ': {username} denied by prior creation attempt failures', [
1676 'username' => $username,
1679 $user->loadFromId();
1680 return Status
::newFatal( 'authmanager-autocreate-exception' );
1683 // Checks passed, create the user...
1684 $from = isset( $_SERVER['REQUEST_URI'] ) ?
$_SERVER['REQUEST_URI'] : 'CLI';
1685 $this->logger
->info( __METHOD__
. ': creating new user ({username}) - from: {from}', [
1686 'username' => $username,
1690 // Ignore warnings about master connections/writes...hard to avoid here
1691 $trxProfiler = \Profiler
::instance()->getTransactionProfiler();
1692 $old = $trxProfiler->setSilenced( true );
1694 $status = $user->addToDatabase();
1695 if ( !$status->isOK() ) {
1696 // Double-check for a race condition (T70012). We make use of the fact that when
1697 // addToDatabase fails due to the user already existing, the user object gets loaded.
1698 if ( $user->getId() ) {
1699 $this->logger
->info( __METHOD__
. ': {username} already exists locally (race)', [
1700 'username' => $username,
1703 $this->setSessionDataForUser( $user );
1705 $status = Status
::newGood();
1706 $status->warning( 'userexists' );
1708 $this->logger
->error( __METHOD__
. ': {username} failed with message {msg}', [
1709 'username' => $username,
1710 'msg' => $status->getWikiText( null, null, 'en' )
1713 $user->loadFromId();
1717 } catch ( \Exception
$ex ) {
1718 $trxProfiler->setSilenced( $old );
1719 $this->logger
->error( __METHOD__
. ': {username} failed with exception {exception}', [
1720 'username' => $username,
1723 // Do not keep throwing errors for a while
1724 $cache->set( $backoffKey, 1, 600 );
1725 // Bubble up error; which should normally trigger DB rollbacks
1729 $this->setDefaultUserOptions( $user, false );
1731 // Inform the providers
1732 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1734 \Hooks
::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1735 \Hooks
::run( 'LocalUserCreated', [ $user, true ] );
1736 $user->saveSettings();
1738 // Update user count
1739 \DeferredUpdates
::addUpdate( \SiteStatsUpdate
::factory( [ 'users' => 1 ] ) );
1740 // Watch user's userpage and talk page
1741 \DeferredUpdates
::addCallableUpdate( function () use ( $user ) {
1742 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1746 if ( $this->config
->get( 'NewUserLog' ) ) {
1747 $logEntry = new \
ManualLogEntry( 'newusers', 'autocreate' );
1748 $logEntry->setPerformer( $user );
1749 $logEntry->setTarget( $user->getUserPage() );
1750 $logEntry->setComment( '' );
1751 $logEntry->setParameters( [
1752 '4::userid' => $user->getId(),
1754 $logEntry->insert();
1757 $trxProfiler->setSilenced( $old );
1760 $this->setSessionDataForUser( $user );
1763 return Status
::newGood();
1769 * @name Account linking
1774 * Determine whether accounts can be linked
1777 public function canLinkAccounts() {
1778 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1779 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
) {
1787 * Start an account linking flow
1789 * @param User $user User being linked
1790 * @param AuthenticationRequest[] $reqs
1791 * @param string $returnToUrl Url that REDIRECT responses should eventually
1793 * @return AuthenticationResponse
1795 public function beginAccountLink( User
$user, array $reqs, $returnToUrl ) {
1796 $session = $this->request
->getSession();
1797 $session->remove( 'AuthManager::accountLinkState' );
1799 if ( !$this->canLinkAccounts() ) {
1800 // Caller should have called canLinkAccounts()
1801 throw new \
LogicException( 'Account linking is not possible' );
1804 if ( $user->getId() === 0 ) {
1805 if ( !User
::isUsableName( $user->getName() ) ) {
1806 $msg = wfMessage( 'noname' );
1808 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1810 return AuthenticationResponse
::newFail( $msg );
1812 foreach ( $reqs as $req ) {
1813 $req->username
= $user->getName();
1814 $req->returnToUrl
= $returnToUrl;
1817 $this->removeAuthenticationSessionData( null );
1819 $providers = $this->getPreAuthenticationProviders();
1820 foreach ( $providers as $id => $provider ) {
1821 $status = $provider->testForAccountLink( $user );
1822 if ( !$status->isGood() ) {
1823 $this->logger
->debug( __METHOD__
. ": Account linking pre-check failed by $id", [
1824 'user' => $user->getName(),
1826 $ret = AuthenticationResponse
::newFail(
1827 Status
::wrap( $status )->getMessage()
1829 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1835 'username' => $user->getName(),
1836 'userid' => $user->getId(),
1837 'returnToUrl' => $returnToUrl,
1839 'continueRequests' => [],
1842 $providers = $this->getPrimaryAuthenticationProviders();
1843 foreach ( $providers as $id => $provider ) {
1844 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider
::TYPE_LINK
) {
1848 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1849 switch ( $res->status
) {
1850 case AuthenticationResponse
::PASS
;
1851 $this->logger
->info( "Account linked to {user} by $id", [
1852 'user' => $user->getName(),
1854 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1857 case AuthenticationResponse
::FAIL
;
1858 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1859 'user' => $user->getName(),
1861 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1864 case AuthenticationResponse
::ABSTAIN
;
1868 case AuthenticationResponse
::REDIRECT
;
1869 case AuthenticationResponse
::UI
;
1870 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1871 'user' => $user->getName(),
1873 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1874 $state['primary'] = $id;
1875 $state['continueRequests'] = $res->neededRequests
;
1876 $session->setSecret( 'AuthManager::accountLinkState', $state );
1877 $session->persist();
1880 // @codeCoverageIgnoreStart
1882 throw new \
DomainException(
1883 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1885 // @codeCoverageIgnoreEnd
1889 $this->logger
->debug( __METHOD__
. ': Account linking failed because no provider accepted', [
1890 'user' => $user->getName(),
1892 $ret = AuthenticationResponse
::newFail(
1893 wfMessage( 'authmanager-link-no-primary' )
1895 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1900 * Continue an account linking flow
1901 * @param AuthenticationRequest[] $reqs
1902 * @return AuthenticationResponse
1904 public function continueAccountLink( array $reqs ) {
1905 $session = $this->request
->getSession();
1907 if ( !$this->canLinkAccounts() ) {
1908 // Caller should have called canLinkAccounts()
1909 $session->remove( 'AuthManager::accountLinkState' );
1910 throw new \
LogicException( 'Account linking is not possible' );
1913 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1914 if ( !is_array( $state ) ) {
1915 return AuthenticationResponse
::newFail(
1916 wfMessage( 'authmanager-link-not-in-progress' )
1919 $state['continueRequests'] = [];
1921 // Step 0: Prepare and validate the input
1923 $user = User
::newFromName( $state['username'], 'usable' );
1924 if ( !is_object( $user ) ) {
1925 $session->remove( 'AuthManager::accountLinkState' );
1926 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1928 if ( $user->getId() != $state['userid'] ) {
1929 throw new \
UnexpectedValueException(
1930 "User \"{$state['username']}\" is valid, but " .
1931 "ID {$user->getId()} != {$state['userid']}!"
1935 foreach ( $reqs as $req ) {
1936 $req->username
= $state['username'];
1937 $req->returnToUrl
= $state['returnToUrl'];
1940 // Step 1: Call the primary again until it succeeds
1942 $provider = $this->getAuthenticationProvider( $state['primary'] );
1943 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1944 // Configuration changed? Force them to start over.
1945 // @codeCoverageIgnoreStart
1946 $ret = AuthenticationResponse
::newFail(
1947 wfMessage( 'authmanager-link-not-in-progress' )
1949 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1950 $session->remove( 'AuthManager::accountLinkState' );
1952 // @codeCoverageIgnoreEnd
1954 $id = $provider->getUniqueId();
1955 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1956 switch ( $res->status
) {
1957 case AuthenticationResponse
::PASS
;
1958 $this->logger
->info( "Account linked to {user} by $id", [
1959 'user' => $user->getName(),
1961 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1962 $session->remove( 'AuthManager::accountLinkState' );
1964 case AuthenticationResponse
::FAIL
;
1965 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1966 'user' => $user->getName(),
1968 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1969 $session->remove( 'AuthManager::accountLinkState' );
1971 case AuthenticationResponse
::REDIRECT
;
1972 case AuthenticationResponse
::UI
;
1973 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1974 'user' => $user->getName(),
1976 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1977 $state['continueRequests'] = $res->neededRequests
;
1978 $session->setSecret( 'AuthManager::accountLinkState', $state );
1981 throw new \
DomainException(
1982 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1985 } catch ( \Exception
$ex ) {
1986 $session->remove( 'AuthManager::accountLinkState' );
1994 * @name Information methods
1999 * Return the applicable list of AuthenticationRequests
2001 * Possible values for $action:
2002 * - ACTION_LOGIN: Valid for passing to beginAuthentication
2003 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
2004 * - ACTION_CREATE: Valid for passing to beginAccountCreation
2005 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
2006 * - ACTION_LINK: Valid for passing to beginAccountLink
2007 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
2008 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
2009 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
2010 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2012 * @param string $action One of the AuthManager::ACTION_* constants
2013 * @param User|null $user User being acted on, instead of the current user.
2014 * @return AuthenticationRequest[]
2016 public function getAuthenticationRequests( $action, User
$user = null ) {
2018 $providerAction = $action;
2020 // Figure out which providers to query
2021 switch ( $action ) {
2022 case self
::ACTION_LOGIN
:
2023 case self
::ACTION_CREATE
:
2024 $providers = $this->getPreAuthenticationProviders() +
2025 $this->getPrimaryAuthenticationProviders() +
2026 $this->getSecondaryAuthenticationProviders();
2029 case self
::ACTION_LOGIN_CONTINUE
:
2030 $state = $this->request
->getSession()->getSecret( 'AuthManager::authnState' );
2031 return is_array( $state ) ?
$state['continueRequests'] : [];
2033 case self
::ACTION_CREATE_CONTINUE
:
2034 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
2035 return is_array( $state ) ?
$state['continueRequests'] : [];
2037 case self
::ACTION_LINK
:
2038 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2039 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2043 case self
::ACTION_UNLINK
:
2044 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2045 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2048 // To providers, unlink and remove are identical.
2049 $providerAction = self
::ACTION_REMOVE
;
2052 case self
::ACTION_LINK_CONTINUE
:
2053 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' );
2054 return is_array( $state ) ?
$state['continueRequests'] : [];
2056 case self
::ACTION_CHANGE
:
2057 case self
::ACTION_REMOVE
:
2058 $providers = $this->getPrimaryAuthenticationProviders() +
2059 $this->getSecondaryAuthenticationProviders();
2062 // @codeCoverageIgnoreStart
2064 throw new \
DomainException( __METHOD__
. ": Invalid action \"$action\"" );
2066 // @codeCoverageIgnoreEnd
2068 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2072 * Internal request lookup for self::getAuthenticationRequests
2074 * @param string $providerAction Action to pass to providers
2075 * @param array $options Options to pass to providers
2076 * @param AuthenticationProvider[] $providers
2077 * @param User|null $user
2078 * @return AuthenticationRequest[]
2080 private function getAuthenticationRequestsInternal(
2081 $providerAction, array $options, array $providers, User
$user = null
2083 $user = $user ?
: \RequestContext
::getMain()->getUser();
2084 $options['username'] = $user->isAnon() ?
null : $user->getName();
2086 // Query them and merge results
2088 foreach ( $providers as $provider ) {
2089 $isPrimary = $provider instanceof PrimaryAuthenticationProvider
;
2090 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2091 $id = $req->getUniqueId();
2093 // If a required request if from a Primary, mark it as "primary-required" instead
2095 if ( $req->required
) {
2096 $req->required
= AuthenticationRequest
::PRIMARY_REQUIRED
;
2101 !isset( $reqs[$id] )
2102 ||
$req->required
=== AuthenticationRequest
::REQUIRED
2103 ||
$reqs[$id] === AuthenticationRequest
::OPTIONAL
2110 // AuthManager has its own req for some actions
2111 switch ( $providerAction ) {
2112 case self
::ACTION_LOGIN
:
2113 $reqs[] = new RememberMeAuthenticationRequest
;
2116 case self
::ACTION_CREATE
:
2117 $reqs[] = new UsernameAuthenticationRequest
;
2118 $reqs[] = new UserDataAuthenticationRequest
;
2119 if ( $options['username'] !== null ) {
2120 $reqs[] = new CreationReasonAuthenticationRequest
;
2121 $options['username'] = null; // Don't fill in the username below
2126 // Fill in reqs data
2127 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2129 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2130 if ( $providerAction === self
::ACTION_CHANGE ||
$providerAction === self
::ACTION_REMOVE
) {
2131 $reqs = array_filter( $reqs, function ( $req ) {
2132 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2136 return array_values( $reqs );
2140 * Set values in an array of requests
2141 * @param AuthenticationRequest[] &$reqs
2142 * @param string $action
2143 * @param string|null $username
2144 * @param bool $forceAction
2146 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2147 foreach ( $reqs as $req ) {
2148 if ( !$req->action ||
$forceAction ) {
2149 $req->action
= $action;
2151 if ( $req->username
=== null ) {
2152 $req->username
= $username;
2158 * Determine whether a username exists
2159 * @param string $username
2160 * @param int $flags Bitfield of User:READ_* constants
2163 public function userExists( $username, $flags = User
::READ_NORMAL
) {
2164 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2165 if ( $provider->testUserExists( $username, $flags ) ) {
2174 * Determine whether a user property should be allowed to be changed.
2176 * Supported properties are:
2181 * @param string $property
2184 public function allowsPropertyChange( $property ) {
2185 $providers = $this->getPrimaryAuthenticationProviders() +
2186 $this->getSecondaryAuthenticationProviders();
2187 foreach ( $providers as $provider ) {
2188 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2196 * Get a provider by ID
2197 * @note This is public so extensions can check whether their own provider
2198 * is installed and so they can read its configuration if necessary.
2199 * Other uses are not recommended.
2201 * @return AuthenticationProvider|null
2203 public function getAuthenticationProvider( $id ) {
2205 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2206 return $this->allAuthenticationProviders
[$id];
2209 // Slow version: instantiate each kind and check
2210 $providers = $this->getPrimaryAuthenticationProviders();
2211 if ( isset( $providers[$id] ) ) {
2212 return $providers[$id];
2214 $providers = $this->getSecondaryAuthenticationProviders();
2215 if ( isset( $providers[$id] ) ) {
2216 return $providers[$id];
2218 $providers = $this->getPreAuthenticationProviders();
2219 if ( isset( $providers[$id] ) ) {
2220 return $providers[$id];
2229 * @name Internal methods
2234 * Store authentication in the current session
2235 * @protected For use by AuthenticationProviders
2236 * @param string $key
2237 * @param mixed $data Must be serializable
2239 public function setAuthenticationSessionData( $key, $data ) {
2240 $session = $this->request
->getSession();
2241 $arr = $session->getSecret( 'authData' );
2242 if ( !is_array( $arr ) ) {
2246 $session->setSecret( 'authData', $arr );
2250 * Fetch authentication data from the current session
2251 * @protected For use by AuthenticationProviders
2252 * @param string $key
2253 * @param mixed $default
2256 public function getAuthenticationSessionData( $key, $default = null ) {
2257 $arr = $this->request
->getSession()->getSecret( 'authData' );
2258 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2266 * Remove authentication data
2267 * @protected For use by AuthenticationProviders
2268 * @param string|null $key If null, all data is removed
2270 public function removeAuthenticationSessionData( $key ) {
2271 $session = $this->request
->getSession();
2272 if ( $key === null ) {
2273 $session->remove( 'authData' );
2275 $arr = $session->getSecret( 'authData' );
2276 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2277 unset( $arr[$key] );
2278 $session->setSecret( 'authData', $arr );
2284 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2285 * @param string $class
2286 * @param array[] $specs
2287 * @return AuthenticationProvider[]
2289 protected function providerArrayFromSpecs( $class, array $specs ) {
2291 foreach ( $specs as &$spec ) {
2292 $spec = [ 'sort2' => $i++
] +
$spec +
[ 'sort' => 0 ];
2295 usort( $specs, function ( $a, $b ) {
2296 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2297 ?
: $a['sort2'] - $b['sort2'];
2301 foreach ( $specs as $spec ) {
2302 $provider = ObjectFactory
::getObjectFromSpec( $spec );
2303 if ( !$provider instanceof $class ) {
2304 throw new \
RuntimeException(
2305 "Expected instance of $class, got " . get_class( $provider )
2308 $provider->setLogger( $this->logger
);
2309 $provider->setManager( $this );
2310 $provider->setConfig( $this->config
);
2311 $id = $provider->getUniqueId();
2312 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2313 throw new \
RuntimeException(
2314 "Duplicate specifications for id $id (classes " .
2315 get_class( $provider ) . ' and ' .
2316 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
2319 $this->allAuthenticationProviders
[$id] = $provider;
2320 $ret[$id] = $provider;
2326 * Get the configuration
2329 private function getConfiguration() {
2330 return $this->config
->get( 'AuthManagerConfig' ) ?
: $this->config
->get( 'AuthManagerAutoConfig' );
2334 * Get the list of PreAuthenticationProviders
2335 * @return PreAuthenticationProvider[]
2337 protected function getPreAuthenticationProviders() {
2338 if ( $this->preAuthenticationProviders
=== null ) {
2339 $conf = $this->getConfiguration();
2340 $this->preAuthenticationProviders
= $this->providerArrayFromSpecs(
2341 PreAuthenticationProvider
::class, $conf['preauth']
2344 return $this->preAuthenticationProviders
;
2348 * Get the list of PrimaryAuthenticationProviders
2349 * @return PrimaryAuthenticationProvider[]
2351 protected function getPrimaryAuthenticationProviders() {
2352 if ( $this->primaryAuthenticationProviders
=== null ) {
2353 $conf = $this->getConfiguration();
2354 $this->primaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2355 PrimaryAuthenticationProvider
::class, $conf['primaryauth']
2358 return $this->primaryAuthenticationProviders
;
2362 * Get the list of SecondaryAuthenticationProviders
2363 * @return SecondaryAuthenticationProvider[]
2365 protected function getSecondaryAuthenticationProviders() {
2366 if ( $this->secondaryAuthenticationProviders
=== null ) {
2367 $conf = $this->getConfiguration();
2368 $this->secondaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2369 SecondaryAuthenticationProvider
::class, $conf['secondaryauth']
2372 return $this->secondaryAuthenticationProviders
;
2378 * @param bool|null $remember
2380 private function setSessionDataForUser( $user, $remember = null ) {
2381 $session = $this->request
->getSession();
2382 $delay = $session->delaySave();
2384 $session->resetId();
2385 $session->resetAllTokens();
2386 if ( $session->canSetUser() ) {
2387 $session->setUser( $user );
2389 if ( $remember !== null ) {
2390 $session->setRememberUser( $remember );
2392 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2393 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2394 $session->persist();
2396 \Wikimedia\ScopedCallback
::consume( $delay );
2398 \Hooks
::run( 'UserLoggedIn', [ $user ] );
2403 * @param bool $useContextLang Use 'uselang' to set the user's language
2405 private function setDefaultUserOptions( User
$user, $useContextLang ) {
2410 $lang = $useContextLang ? \RequestContext
::getMain()->getLanguage() : $wgContLang;
2411 $user->setOption( 'language', $lang->getPreferredVariant() );
2413 if ( $wgContLang->hasVariants() ) {
2414 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2419 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2420 * @param string $method
2421 * @param array $args
2423 private function callMethodOnProviders( $which, $method, array $args ) {
2426 $providers +
= $this->getPreAuthenticationProviders();
2429 $providers +
= $this->getPrimaryAuthenticationProviders();
2432 $providers +
= $this->getSecondaryAuthenticationProviders();
2434 foreach ( $providers as $provider ) {
2435 call_user_func_array( [ $provider, $method ], $args );
2440 * Reset the internal caching for unit testing
2441 * @protected Unit tests only
2443 public static function resetCache() {
2444 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2445 // @codeCoverageIgnoreStart
2446 throw new \
MWException( __METHOD__
. ' may only be called from unit tests!' );
2447 // @codeCoverageIgnoreEnd
2450 self
::$instance = null;
2458 * For really cool vim folding this needs to be at the end:
2459 * vim: foldmarker=@{,@} foldmethod=marker