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;
36 * This serves as the entry point to the authentication system.
38 * In the future, it may also serve as the entry point to the authorization
41 * If you are looking at this because you are working on an extension that creates its own
42 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
43 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
44 * or the createaccount API. Trying to call this class directly will very likely end up in
45 * security vulnerabilities or broken UX in edge cases.
47 * If you are working on an extension that needs to integrate with the authentication system
48 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
49 * need to write an AuthenticationProvider.
51 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
52 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
53 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
54 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
55 * responsibility to ensure that the user can authenticate somehow (see especially
56 * PrimaryAuthenticationProvider::autoCreatedAccount()).
57 * If you are writing code that is not associated with such a provider and needs to create accounts
58 * programmatically for real users, you should rethink your architecture. There is no good way to
59 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
60 * cannot provide any means for users to access the accounts it would create.
62 * The two main control flows when using this class are as follows:
63 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
64 * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
65 * exposing a form specification via the API, so that the client can build it), and pass them to
66 * the appropriate begin* method. That will return either a success/failure response, or more
67 * requests to fill (either by building a form or by redirecting the user to some external
68 * provider which will send the data back), in which case they need to be submitted to the
69 * appropriate continue* method and that step has to be repeated until the response is a success
70 * or failure response. AuthManager will use the session to maintain internal state during the
72 * * Code doing an authentication data change will call getAuthenticationRequests(), select
73 * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
74 * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
75 * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
80 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
82 class AuthManager implements LoggerAwareInterface {
83 /** Log in with an existing (not necessarily local) user */
84 const ACTION_LOGIN = 'login';
85 /** Continue a login process that was interrupted by the need for user input or communication
86 * with an external provider */
87 const ACTION_LOGIN_CONTINUE = 'login-continue';
88 /** Create a new user */
89 const ACTION_CREATE = 'create';
90 /** Continue a user creation process that was interrupted by the need for user input or
91 * communication with an external provider */
92 const ACTION_CREATE_CONTINUE = 'create-continue';
93 /** Link an existing user to a third-party account */
94 const ACTION_LINK = 'link';
95 /** Continue a user linking process that was interrupted by the need for user input or
96 * communication with an external provider */
97 const ACTION_LINK_CONTINUE = 'link-continue';
98 /** Change a user's credentials */
99 const ACTION_CHANGE = 'change';
100 /** Remove a user's credentials */
101 const ACTION_REMOVE = 'remove';
102 /** Like ACTION_REMOVE but for linking providers only */
103 const ACTION_UNLINK = 'unlink';
105 /** Security-sensitive operations are ok. */
107 /** Security-sensitive operations should re-authenticate. */
108 const SEC_REAUTH = 'reauth';
109 /** Security-sensitive should not be performed. */
110 const SEC_FAIL = 'fail';
112 /** Auto-creation is due to SessionManager */
113 const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
115 /** @var AuthManager|null */
116 private static $instance = null;
118 /** @var WebRequest */
124 /** @var LoggerInterface */
127 /** @var AuthenticationProvider[] */
128 private $allAuthenticationProviders = [];
130 /** @var PreAuthenticationProvider[] */
131 private $preAuthenticationProviders = null;
133 /** @var PrimaryAuthenticationProvider[] */
134 private $primaryAuthenticationProviders = null;
136 /** @var SecondaryAuthenticationProvider[] */
137 private $secondaryAuthenticationProviders = null;
139 /** @var CreatedAccountAuthenticationRequest[] */
140 private $createdAccountAuthenticationRequests = [];
143 * Get the global AuthManager
144 * @return AuthManager
146 public static function singleton() {
147 if ( self::$instance === null ) {
148 self::$instance = new self(
149 \RequestContext::getMain()->getRequest(),
150 MediaWikiServices::getInstance()->getMainConfig()
153 return self::$instance;
157 * @param WebRequest $request
158 * @param Config $config
160 public function __construct( WebRequest $request, Config $config ) {
161 $this->request = $request;
162 $this->config = $config;
163 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
167 * @param LoggerInterface $logger
169 public function setLogger( LoggerInterface $logger ) {
170 $this->logger = $logger;
176 public function getRequest() {
177 return $this->request;
181 * Force certain PrimaryAuthenticationProviders
182 * @deprecated For backwards compatibility only
183 * @param PrimaryAuthenticationProvider[] $providers
186 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
187 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
189 if ( $this->primaryAuthenticationProviders !== null ) {
190 $this->logger->warning(
191 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
194 $this->allAuthenticationProviders = array_diff_key(
195 $this->allAuthenticationProviders,
196 $this->primaryAuthenticationProviders
198 $session = $this->request->getSession();
199 $session->remove( 'AuthManager::authnState' );
200 $session->remove( 'AuthManager::accountCreationState' );
201 $session->remove( 'AuthManager::accountLinkState' );
202 $this->createdAccountAuthenticationRequests = [];
205 $this->primaryAuthenticationProviders = [];
206 foreach ( $providers as $provider ) {
207 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
208 throw new \RuntimeException(
209 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
210 get_class( $provider )
213 $provider->setLogger( $this->logger );
214 $provider->setManager( $this );
215 $provider->setConfig( $this->config );
216 $id = $provider->getUniqueId();
217 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
218 throw new \RuntimeException(
219 "Duplicate specifications for id $id (classes " .
220 get_class( $provider ) . ' and ' .
221 get_class( $this->allAuthenticationProviders[$id] ) . ')'
224 $this->allAuthenticationProviders[$id] = $provider;
225 $this->primaryAuthenticationProviders[$id] = $provider;
230 * Call a legacy AuthPlugin method, if necessary
231 * @codeCoverageIgnore
232 * @deprecated For backwards compatibility only, should be avoided in new code
233 * @param string $method AuthPlugin method to call
234 * @param array $params Parameters to pass
235 * @param mixed $return Return value if AuthPlugin wasn't called
236 * @return mixed Return value from the AuthPlugin method, or $return
238 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
241 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
242 return call_user_func_array( [ $wgAuth, $method ], $params );
249 * @name Authentication
254 * Indicate whether user authentication is possible
256 * It may not be if the session is provided by something like OAuth
257 * for which each individual request includes authentication data.
261 public function canAuthenticateNow() {
262 return $this->request->getSession()->canSetUser();
266 * Start an authentication flow
268 * In addition to the AuthenticationRequests returned by
269 * $this->getAuthenticationRequests(), a client might include a
270 * CreateFromLoginAuthenticationRequest from a previous login attempt to
273 * Instead of the AuthenticationRequests returned by
274 * $this->getAuthenticationRequests(), a client might pass a
275 * CreatedAccountAuthenticationRequest from an account creation that just
276 * succeeded to log in to the just-created account.
278 * @param AuthenticationRequest[] $reqs
279 * @param string $returnToUrl Url that REDIRECT responses should eventually
281 * @return AuthenticationResponse See self::continueAuthentication()
283 public function beginAuthentication( array $reqs, $returnToUrl ) {
284 $session = $this->request->getSession();
285 if ( !$session->canSetUser() ) {
286 // Caller should have called canAuthenticateNow()
287 $session->remove( 'AuthManager::authnState' );
288 throw new \LogicException( 'Authentication is not possible now' );
291 $guessUserName = null;
292 foreach ( $reqs as $req ) {
293 $req->returnToUrl = $returnToUrl;
294 // @codeCoverageIgnoreStart
295 if ( $req->username !== null && $req->username !== '' ) {
296 if ( $guessUserName === null ) {
297 $guessUserName = $req->username;
298 } elseif ( $guessUserName !== $req->username ) {
299 $guessUserName = null;
303 // @codeCoverageIgnoreEnd
306 // Check for special-case login of a just-created account
307 $req = AuthenticationRequest::getRequestByClass(
308 $reqs, CreatedAccountAuthenticationRequest::class
311 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
312 throw new \LogicException(
313 'CreatedAccountAuthenticationRequests are only valid on ' .
314 'the same AuthManager that created the account'
318 $user = User::newFromName( $req->username );
319 // @codeCoverageIgnoreStart
321 throw new \UnexpectedValueException(
322 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
324 } elseif ( $user->getId() != $req->id ) {
325 throw new \UnexpectedValueException(
326 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
329 // @codeCoverageIgnoreEnd
331 $this->logger->info( 'Logging in {user} after account creation', [
332 'user' => $user->getName(),
334 $ret = AuthenticationResponse::newPass( $user->getName() );
335 $this->setSessionDataForUser( $user );
336 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
337 $session->remove( 'AuthManager::authnState' );
338 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
342 $this->removeAuthenticationSessionData( null );
344 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
345 $status = $provider->testForAuthentication( $reqs );
346 if ( !$status->isGood() ) {
347 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
348 $ret = AuthenticationResponse::newFail(
349 Status::wrap( $status )->getMessage()
351 $this->callMethodOnProviders( 7, 'postAuthentication',
352 [ User::newFromName( $guessUserName ) ?: null, $ret ]
354 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
361 'returnToUrl' => $returnToUrl,
362 'guessUserName' => $guessUserName,
364 'primaryResponse' => null,
367 'continueRequests' => [],
370 // Preserve state from a previous failed login
371 $req = AuthenticationRequest::getRequestByClass(
372 $reqs, CreateFromLoginAuthenticationRequest::class
375 $state['maybeLink'] = $req->maybeLink;
378 $session = $this->request->getSession();
379 $session->setSecret( 'AuthManager::authnState', $state );
382 return $this->continueAuthentication( $reqs );
386 * Continue an authentication flow
388 * Return values are interpreted as follows:
389 * - status FAIL: Authentication failed. If $response->createRequest is
390 * set, that may be passed to self::beginAuthentication() or to
391 * self::beginAccountCreation() to preserve state.
392 * - status REDIRECT: The client should be redirected to the contained URL,
393 * new AuthenticationRequests should be made (if any), then
394 * AuthManager::continueAuthentication() should be called.
395 * - status UI: The client should be presented with a user interface for
396 * the fields in the specified AuthenticationRequests, then new
397 * AuthenticationRequests should be made, then
398 * AuthManager::continueAuthentication() should be called.
399 * - status RESTART: The user logged in successfully with a third-party
400 * service, but the third-party credentials aren't attached to any local
401 * account. This could be treated as a UI or a FAIL.
402 * - status PASS: Authentication was successful.
404 * @param AuthenticationRequest[] $reqs
405 * @return AuthenticationResponse
407 public function continueAuthentication( array $reqs ) {
408 $session = $this->request->getSession();
410 if ( !$session->canSetUser() ) {
411 // Caller should have called canAuthenticateNow()
412 // @codeCoverageIgnoreStart
413 throw new \LogicException( 'Authentication is not possible now' );
414 // @codeCoverageIgnoreEnd
417 $state = $session->getSecret( 'AuthManager::authnState' );
418 if ( !is_array( $state ) ) {
419 return AuthenticationResponse::newFail(
420 wfMessage( 'authmanager-authn-not-in-progress' )
423 $state['continueRequests'] = [];
425 $guessUserName = $state['guessUserName'];
427 foreach ( $reqs as $req ) {
428 $req->returnToUrl = $state['returnToUrl'];
431 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
433 if ( $state['primary'] === null ) {
434 // We haven't picked a PrimaryAuthenticationProvider yet
435 // @codeCoverageIgnoreStart
436 $guessUserName = null;
437 foreach ( $reqs as $req ) {
438 if ( $req->username !== null && $req->username !== '' ) {
439 if ( $guessUserName === null ) {
440 $guessUserName = $req->username;
441 } elseif ( $guessUserName !== $req->username ) {
442 $guessUserName = null;
447 $state['guessUserName'] = $guessUserName;
448 // @codeCoverageIgnoreEnd
449 $state['reqs'] = $reqs;
451 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
452 $res = $provider->beginPrimaryAuthentication( $reqs );
453 switch ( $res->status ) {
454 case AuthenticationResponse::PASS;
455 $state['primary'] = $id;
456 $state['primaryResponse'] = $res;
457 $this->logger->debug( "Primary login with $id succeeded" );
459 case AuthenticationResponse::FAIL;
460 $this->logger->debug( "Login failed in primary authentication by $id" );
461 if ( $res->createRequest || $state['maybeLink'] ) {
462 $res->createRequest = new CreateFromLoginAuthenticationRequest(
463 $res->createRequest, $state['maybeLink']
466 $this->callMethodOnProviders( 7, 'postAuthentication',
467 [ User::newFromName( $guessUserName ) ?: null, $res ]
469 $session->remove( 'AuthManager::authnState' );
470 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
472 case AuthenticationResponse::ABSTAIN;
475 case AuthenticationResponse::REDIRECT;
476 case AuthenticationResponse::UI;
477 $this->logger->debug( "Primary login with $id returned $res->status" );
478 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
479 $state['primary'] = $id;
480 $state['continueRequests'] = $res->neededRequests;
481 $session->setSecret( 'AuthManager::authnState', $state );
484 // @codeCoverageIgnoreStart
486 throw new \DomainException(
487 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
489 // @codeCoverageIgnoreEnd
492 if ( $state['primary'] === null ) {
493 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
494 $ret = AuthenticationResponse::newFail(
495 wfMessage( 'authmanager-authn-no-primary' )
497 $this->callMethodOnProviders( 7, 'postAuthentication',
498 [ User::newFromName( $guessUserName ) ?: null, $ret ]
500 $session->remove( 'AuthManager::authnState' );
503 } elseif ( $state['primaryResponse'] === null ) {
504 $provider = $this->getAuthenticationProvider( $state['primary'] );
505 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
506 // Configuration changed? Force them to start over.
507 // @codeCoverageIgnoreStart
508 $ret = AuthenticationResponse::newFail(
509 wfMessage( 'authmanager-authn-not-in-progress' )
511 $this->callMethodOnProviders( 7, 'postAuthentication',
512 [ User::newFromName( $guessUserName ) ?: null, $ret ]
514 $session->remove( 'AuthManager::authnState' );
516 // @codeCoverageIgnoreEnd
518 $id = $provider->getUniqueId();
519 $res = $provider->continuePrimaryAuthentication( $reqs );
520 switch ( $res->status ) {
521 case AuthenticationResponse::PASS;
522 $state['primaryResponse'] = $res;
523 $this->logger->debug( "Primary login with $id succeeded" );
525 case AuthenticationResponse::FAIL;
526 $this->logger->debug( "Login failed in primary authentication by $id" );
527 if ( $res->createRequest || $state['maybeLink'] ) {
528 $res->createRequest = new CreateFromLoginAuthenticationRequest(
529 $res->createRequest, $state['maybeLink']
532 $this->callMethodOnProviders( 7, 'postAuthentication',
533 [ User::newFromName( $guessUserName ) ?: null, $res ]
535 $session->remove( 'AuthManager::authnState' );
536 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
538 case AuthenticationResponse::REDIRECT;
539 case AuthenticationResponse::UI;
540 $this->logger->debug( "Primary login with $id returned $res->status" );
541 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
542 $state['continueRequests'] = $res->neededRequests;
543 $session->setSecret( 'AuthManager::authnState', $state );
546 throw new \DomainException(
547 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
552 $res = $state['primaryResponse'];
553 if ( $res->username === null ) {
554 $provider = $this->getAuthenticationProvider( $state['primary'] );
555 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
556 // Configuration changed? Force them to start over.
557 // @codeCoverageIgnoreStart
558 $ret = AuthenticationResponse::newFail(
559 wfMessage( 'authmanager-authn-not-in-progress' )
561 $this->callMethodOnProviders( 7, 'postAuthentication',
562 [ User::newFromName( $guessUserName ) ?: null, $ret ]
564 $session->remove( 'AuthManager::authnState' );
566 // @codeCoverageIgnoreEnd
569 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
571 // don't confuse the user with an incorrect message if linking is disabled
572 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
574 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
575 $msg = 'authmanager-authn-no-local-user-link';
577 $msg = 'authmanager-authn-no-local-user';
579 $this->logger->debug(
580 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
582 $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
583 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
586 $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
588 if ( $res->createRequest || $state['maybeLink'] ) {
589 $ret->createRequest = new CreateFromLoginAuthenticationRequest(
590 $res->createRequest, $state['maybeLink']
592 $ret->neededRequests[] = $ret->createRequest;
594 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
595 $session->setSecret( 'AuthManager::authnState', [
596 'reqs' => [], // Will be filled in later
598 'primaryResponse' => null,
600 'continueRequests' => $ret->neededRequests,
605 // Step 2: Primary authentication succeeded, create the User object
606 // (and add the user locally if necessary)
608 $user = User::newFromName( $res->username, 'usable' );
610 $provider = $this->getAuthenticationProvider( $state['primary'] );
611 throw new \DomainException(
612 get_class( $provider ) . " returned an invalid username: {$res->username}"
615 if ( $user->getId() === 0 ) {
616 // User doesn't exist locally. Create it.
617 $this->logger->info( 'Auto-creating {user} on login', [
618 'user' => $user->getName(),
620 $status = $this->autoCreateUser( $user, $state['primary'], false );
621 if ( !$status->isGood() ) {
622 $ret = AuthenticationResponse::newFail(
623 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
625 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
626 $session->remove( 'AuthManager::authnState' );
627 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
632 // Step 3: Iterate over all the secondary authentication providers.
634 $beginReqs = $state['reqs'];
636 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
637 if ( !isset( $state['secondary'][$id] ) ) {
638 // This provider isn't started yet, so we pass it the set
639 // of reqs from beginAuthentication instead of whatever
640 // might have been used by a previous provider in line.
641 $func = 'beginSecondaryAuthentication';
642 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
643 } elseif ( !$state['secondary'][$id] ) {
644 $func = 'continueSecondaryAuthentication';
645 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
649 switch ( $res->status ) {
650 case AuthenticationResponse::PASS;
651 $this->logger->debug( "Secondary login with $id succeeded" );
653 case AuthenticationResponse::ABSTAIN;
654 $state['secondary'][$id] = true;
656 case AuthenticationResponse::FAIL;
657 $this->logger->debug( "Login failed in secondary authentication by $id" );
658 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
659 $session->remove( 'AuthManager::authnState' );
660 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
662 case AuthenticationResponse::REDIRECT;
663 case AuthenticationResponse::UI;
664 $this->logger->debug( "Secondary login with $id returned " . $res->status );
665 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
666 $state['secondary'][$id] = false;
667 $state['continueRequests'] = $res->neededRequests;
668 $session->setSecret( 'AuthManager::authnState', $state );
671 // @codeCoverageIgnoreStart
673 throw new \DomainException(
674 get_class( $provider ) . "::{$func}() returned $res->status"
676 // @codeCoverageIgnoreEnd
680 // Step 4: Authentication complete! Set the user in the session and
683 $this->logger->info( 'Login for {user} succeeded from {clientip}', [
684 'user' => $user->getName(),
685 'clientip' => $this->request->getIP(),
687 /** @var RememberMeAuthenticationRequest $req */
688 $req = AuthenticationRequest::getRequestByClass(
689 $beginReqs, RememberMeAuthenticationRequest::class
691 $this->setSessionDataForUser( $user, $req && $req->rememberMe );
692 $ret = AuthenticationResponse::newPass( $user->getName() );
693 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
694 $session->remove( 'AuthManager::authnState' );
695 $this->removeAuthenticationSessionData( null );
696 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
698 } catch ( \Exception $ex ) {
699 $session->remove( 'AuthManager::authnState' );
705 * Whether security-sensitive operations should proceed.
707 * A "security-sensitive operation" is something like a password or email
708 * change, that would normally have a "reenter your password to confirm"
709 * box if we only supported password-based authentication.
711 * @param string $operation Operation being checked. This should be a
712 * message-key-like string such as 'change-password' or 'change-email'.
713 * @return string One of the SEC_* constants.
715 public function securitySensitiveOperationStatus( $operation ) {
716 $status = self::SEC_OK;
718 $this->logger->debug( __METHOD__ . ": Checking $operation" );
720 $session = $this->request->getSession();
721 $aId = $session->getUser()->getId();
723 // User isn't authenticated. DWIM?
724 $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
725 $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
729 if ( $session->canSetUser() ) {
730 $id = $session->get( 'AuthManager:lastAuthId' );
731 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
732 if ( $id !== $aId || $last === null ) {
733 $timeSinceLogin = PHP_INT_MAX; // Forever ago
735 $timeSinceLogin = max( 0, time() - $last );
738 $thresholds = $this->config->get( 'ReauthenticateTime' );
739 if ( isset( $thresholds[$operation] ) ) {
740 $threshold = $thresholds[$operation];
741 } elseif ( isset( $thresholds['default'] ) ) {
742 $threshold = $thresholds['default'];
744 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
747 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
748 $status = self::SEC_REAUTH;
751 $timeSinceLogin = -1;
753 $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
754 if ( isset( $pass[$operation] ) ) {
755 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
756 } elseif ( isset( $pass['default'] ) ) {
757 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
759 throw new \UnexpectedValueException(
760 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
765 \Hooks::run( 'SecuritySensitiveOperationStatus', [
766 &$status, $operation, $session, $timeSinceLogin
769 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
770 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
771 $status = self::SEC_FAIL;
774 $this->logger->info( __METHOD__ . ": $operation is $status" );
780 * Determine whether a username can authenticate
782 * This is mainly for internal purposes and only takes authentication data into account,
783 * not things like blocks that can change without the authentication system being aware.
785 * @param string $username MediaWiki username
788 public function userCanAuthenticate( $username ) {
789 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
790 if ( $provider->testUserCanAuthenticate( $username ) ) {
798 * Provide normalized versions of the username for security checks
800 * Since different providers can normalize the input in different ways,
801 * this returns an array of all the different ways the name might be
802 * normalized for authentication.
804 * The returned strings should not be revealed to the user, as that might
805 * leak private information (e.g. an email address might be normalized to a
808 * @param string $username
811 public function normalizeUsername( $username ) {
813 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
814 $normalized = $provider->providerNormalizeUsername( $username );
815 if ( $normalized !== null ) {
816 $ret[$normalized] = true;
819 return array_keys( $ret );
825 * @name Authentication data changing
830 * Revoke any authentication credentials for a user
832 * After this, the user should no longer be able to log in.
834 * @param string $username
836 public function revokeAccessForUser( $username ) {
837 $this->logger->info( 'Revoking access for {user}', [
840 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
844 * Validate a change of authentication data (e.g. passwords)
845 * @param AuthenticationRequest $req
846 * @param bool $checkData If false, $req hasn't been loaded from the
847 * submission so checks on user-submitted fields should be skipped. $req->username is
848 * considered user-submitted for this purpose, even if it cannot be changed via
849 * $req->loadFromSubmission.
852 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
854 $providers = $this->getPrimaryAuthenticationProviders() +
855 $this->getSecondaryAuthenticationProviders();
856 foreach ( $providers as $provider ) {
857 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
858 if ( !$status->isGood() ) {
859 return Status::wrap( $status );
861 $any = $any || $status->value !== 'ignored';
864 $status = Status::newGood( 'ignored' );
865 $status->warning( 'authmanager-change-not-supported' );
868 return Status::newGood();
872 * Change authentication data (e.g. passwords)
874 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
875 * result in a successful login in the future.
877 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
878 * no longer result in a successful login.
880 * This method should only be called if allowsAuthenticationDataChange( $req, true )
883 * @param AuthenticationRequest $req
885 public function changeAuthenticationData( AuthenticationRequest $req ) {
886 $this->logger->info( 'Changing authentication data for {user} class {what}', [
887 'user' => is_string( $req->username ) ? $req->username : '<no name>',
888 'what' => get_class( $req ),
891 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
893 // When the main account's authentication data is changed, invalidate
894 // all BotPasswords too.
895 \BotPassword::invalidateAllPasswordsForUser( $req->username );
901 * @name Account creation
906 * Determine whether accounts can be created
909 public function canCreateAccounts() {
910 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
911 switch ( $provider->accountCreationType() ) {
912 case PrimaryAuthenticationProvider::TYPE_CREATE:
913 case PrimaryAuthenticationProvider::TYPE_LINK:
921 * Determine whether a particular account can be created
922 * @param string $username MediaWiki username
923 * @param array $options
924 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
925 * - creating: (bool) For internal use only. Never specify this.
928 public function canCreateAccount( $username, $options = [] ) {
930 if ( is_int( $options ) ) {
931 $options = [ 'flags' => $options ];
934 'flags' => User::READ_NORMAL,
937 $flags = $options['flags'];
939 if ( !$this->canCreateAccounts() ) {
940 return Status::newFatal( 'authmanager-create-disabled' );
943 if ( $this->userExists( $username, $flags ) ) {
944 return Status::newFatal( 'userexists' );
947 $user = User::newFromName( $username, 'creatable' );
948 if ( !is_object( $user ) ) {
949 return Status::newFatal( 'noname' );
951 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
952 if ( $user->getId() !== 0 ) {
953 return Status::newFatal( 'userexists' );
957 // Denied by providers?
958 $providers = $this->getPreAuthenticationProviders() +
959 $this->getPrimaryAuthenticationProviders() +
960 $this->getSecondaryAuthenticationProviders();
961 foreach ( $providers as $provider ) {
962 $status = $provider->testUserForCreation( $user, false, $options );
963 if ( !$status->isGood() ) {
964 return Status::wrap( $status );
968 return Status::newGood();
972 * Basic permissions checks on whether a user can create accounts
973 * @param User $creator User doing the account creation
976 public function checkAccountCreatePermissions( User $creator ) {
977 // Wiki is read-only?
978 if ( wfReadOnly() ) {
979 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
982 // This is awful, this permission check really shouldn't go through Title.
983 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
984 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
986 $status = Status::newGood();
987 foreach ( $permErrors as $args ) {
988 call_user_func_array( [ $status, 'fatal' ], $args );
993 $block = $creator->isBlockedFromCreateAccount();
997 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
1001 if ( $block->getType() === \Block::TYPE_RANGE ) {
1002 $errorMessage = 'cantcreateaccount-range-text';
1003 $errorParams[] = $this->getRequest()->getIP();
1005 $errorMessage = 'cantcreateaccount-text';
1008 return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
1011 $ip = $this->getRequest()->getIP();
1012 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1013 return Status::newFatal( 'sorbs_create_account_reason' );
1016 return Status::newGood();
1020 * Start an account creation flow
1022 * In addition to the AuthenticationRequests returned by
1023 * $this->getAuthenticationRequests(), a client might include a
1024 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1026 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1028 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1029 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1030 * username set, that username must be used for all other requests.
1032 * @param User $creator User doing the account creation
1033 * @param AuthenticationRequest[] $reqs
1034 * @param string $returnToUrl Url that REDIRECT responses should eventually
1036 * @return AuthenticationResponse
1038 public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
1039 $session = $this->request->getSession();
1040 if ( !$this->canCreateAccounts() ) {
1041 // Caller should have called canCreateAccounts()
1042 $session->remove( 'AuthManager::accountCreationState' );
1043 throw new \LogicException( 'Account creation is not possible' );
1047 $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1048 } catch ( \UnexpectedValueException $ex ) {
1051 if ( $username === null ) {
1052 $this->logger->debug( __METHOD__ . ': No username provided' );
1053 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1056 // Permissions check
1057 $status = $this->checkAccountCreatePermissions( $creator );
1058 if ( !$status->isGood() ) {
1059 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1060 'user' => $username,
1061 'creator' => $creator->getName(),
1062 'reason' => $status->getWikiText( null, null, 'en' )
1064 return AuthenticationResponse::newFail( $status->getMessage() );
1067 $status = $this->canCreateAccount(
1068 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1070 if ( !$status->isGood() ) {
1071 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1072 'user' => $username,
1073 'creator' => $creator->getName(),
1074 'reason' => $status->getWikiText( null, null, 'en' )
1076 return AuthenticationResponse::newFail( $status->getMessage() );
1079 $user = User::newFromName( $username, 'creatable' );
1080 foreach ( $reqs as $req ) {
1081 $req->username = $username;
1082 $req->returnToUrl = $returnToUrl;
1083 if ( $req instanceof UserDataAuthenticationRequest ) {
1084 $status = $req->populateUser( $user );
1085 if ( !$status->isGood() ) {
1086 $status = Status::wrap( $status );
1087 $session->remove( 'AuthManager::accountCreationState' );
1088 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1089 'user' => $user->getName(),
1090 'creator' => $creator->getName(),
1091 'reason' => $status->getWikiText( null, null, 'en' ),
1093 return AuthenticationResponse::newFail( $status->getMessage() );
1098 $this->removeAuthenticationSessionData( null );
1101 'username' => $username,
1103 'creatorid' => $creator->getId(),
1104 'creatorname' => $creator->getName(),
1106 'returnToUrl' => $returnToUrl,
1108 'primaryResponse' => null,
1110 'continueRequests' => [],
1112 'ranPreTests' => false,
1115 // Special case: converting a login to an account creation
1116 $req = AuthenticationRequest::getRequestByClass(
1117 $reqs, CreateFromLoginAuthenticationRequest::class
1120 $state['maybeLink'] = $req->maybeLink;
1122 if ( $req->createRequest ) {
1123 $reqs[] = $req->createRequest;
1124 $state['reqs'][] = $req->createRequest;
1128 $session->setSecret( 'AuthManager::accountCreationState', $state );
1129 $session->persist();
1131 return $this->continueAccountCreation( $reqs );
1135 * Continue an account creation flow
1136 * @param AuthenticationRequest[] $reqs
1137 * @return AuthenticationResponse
1139 public function continueAccountCreation( array $reqs ) {
1140 $session = $this->request->getSession();
1142 if ( !$this->canCreateAccounts() ) {
1143 // Caller should have called canCreateAccounts()
1144 $session->remove( 'AuthManager::accountCreationState' );
1145 throw new \LogicException( 'Account creation is not possible' );
1148 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1149 if ( !is_array( $state ) ) {
1150 return AuthenticationResponse::newFail(
1151 wfMessage( 'authmanager-create-not-in-progress' )
1154 $state['continueRequests'] = [];
1156 // Step 0: Prepare and validate the input
1158 $user = User::newFromName( $state['username'], 'creatable' );
1159 if ( !is_object( $user ) ) {
1160 $session->remove( 'AuthManager::accountCreationState' );
1161 $this->logger->debug( __METHOD__ . ': Invalid username', [
1162 'user' => $state['username'],
1164 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1167 if ( $state['creatorid'] ) {
1168 $creator = User::newFromId( $state['creatorid'] );
1170 $creator = new User;
1171 $creator->setName( $state['creatorname'] );
1174 // Avoid account creation races on double submissions
1175 $cache = \ObjectCache::getLocalClusterInstance();
1176 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1178 // Don't clear AuthManager::accountCreationState for this code
1179 // path because the process that won the race owns it.
1180 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1181 'user' => $user->getName(),
1182 'creator' => $creator->getName(),
1184 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1187 // Permissions check
1188 $status = $this->checkAccountCreatePermissions( $creator );
1189 if ( !$status->isGood() ) {
1190 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1191 'user' => $user->getName(),
1192 'creator' => $creator->getName(),
1193 'reason' => $status->getWikiText( null, null, 'en' )
1195 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1196 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1197 $session->remove( 'AuthManager::accountCreationState' );
1201 // Load from master for existence check
1202 $user->load( User::READ_LOCKING );
1204 if ( $state['userid'] === 0 ) {
1205 if ( $user->getId() != 0 ) {
1206 $this->logger->debug( __METHOD__ . ': User exists locally', [
1207 'user' => $user->getName(),
1208 'creator' => $creator->getName(),
1210 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1211 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1212 $session->remove( 'AuthManager::accountCreationState' );
1216 if ( $user->getId() == 0 ) {
1217 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1218 'user' => $user->getName(),
1219 'creator' => $creator->getName(),
1220 'expected_id' => $state['userid'],
1222 throw new \UnexpectedValueException(
1223 "User \"{$state['username']}\" should exist now, but doesn't!"
1226 if ( $user->getId() != $state['userid'] ) {
1227 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1228 'user' => $user->getName(),
1229 'creator' => $creator->getName(),
1230 'expected_id' => $state['userid'],
1231 'actual_id' => $user->getId(),
1233 throw new \UnexpectedValueException(
1234 "User \"{$state['username']}\" exists, but " .
1235 "ID {$user->getId()} != {$state['userid']}!"
1239 foreach ( $state['reqs'] as $req ) {
1240 if ( $req instanceof UserDataAuthenticationRequest ) {
1241 $status = $req->populateUser( $user );
1242 if ( !$status->isGood() ) {
1243 // This should never happen...
1244 $status = Status::wrap( $status );
1245 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1246 'user' => $user->getName(),
1247 'creator' => $creator->getName(),
1248 'reason' => $status->getWikiText( null, null, 'en' ),
1250 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1251 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1252 $session->remove( 'AuthManager::accountCreationState' );
1258 foreach ( $reqs as $req ) {
1259 $req->returnToUrl = $state['returnToUrl'];
1260 $req->username = $state['username'];
1263 // Run pre-creation tests, if we haven't already
1264 if ( !$state['ranPreTests'] ) {
1265 $providers = $this->getPreAuthenticationProviders() +
1266 $this->getPrimaryAuthenticationProviders() +
1267 $this->getSecondaryAuthenticationProviders();
1268 foreach ( $providers as $id => $provider ) {
1269 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1270 if ( !$status->isGood() ) {
1271 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1272 'user' => $user->getName(),
1273 'creator' => $creator->getName(),
1275 $ret = AuthenticationResponse::newFail(
1276 Status::wrap( $status )->getMessage()
1278 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1279 $session->remove( 'AuthManager::accountCreationState' );
1284 $state['ranPreTests'] = true;
1287 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1289 if ( $state['primary'] === null ) {
1290 // We haven't picked a PrimaryAuthenticationProvider yet
1291 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1292 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1295 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1296 switch ( $res->status ) {
1297 case AuthenticationResponse::PASS;
1298 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1299 'user' => $user->getName(),
1300 'creator' => $creator->getName(),
1302 $state['primary'] = $id;
1303 $state['primaryResponse'] = $res;
1305 case AuthenticationResponse::FAIL;
1306 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1307 'user' => $user->getName(),
1308 'creator' => $creator->getName(),
1310 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1311 $session->remove( 'AuthManager::accountCreationState' );
1313 case AuthenticationResponse::ABSTAIN;
1316 case AuthenticationResponse::REDIRECT;
1317 case AuthenticationResponse::UI;
1318 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1319 'user' => $user->getName(),
1320 'creator' => $creator->getName(),
1322 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1323 $state['primary'] = $id;
1324 $state['continueRequests'] = $res->neededRequests;
1325 $session->setSecret( 'AuthManager::accountCreationState', $state );
1328 // @codeCoverageIgnoreStart
1330 throw new \DomainException(
1331 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1333 // @codeCoverageIgnoreEnd
1336 if ( $state['primary'] === null ) {
1337 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1338 'user' => $user->getName(),
1339 'creator' => $creator->getName(),
1341 $ret = AuthenticationResponse::newFail(
1342 wfMessage( 'authmanager-create-no-primary' )
1344 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1345 $session->remove( 'AuthManager::accountCreationState' );
1348 } elseif ( $state['primaryResponse'] === null ) {
1349 $provider = $this->getAuthenticationProvider( $state['primary'] );
1350 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1351 // Configuration changed? Force them to start over.
1352 // @codeCoverageIgnoreStart
1353 $ret = AuthenticationResponse::newFail(
1354 wfMessage( 'authmanager-create-not-in-progress' )
1356 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1357 $session->remove( 'AuthManager::accountCreationState' );
1359 // @codeCoverageIgnoreEnd
1361 $id = $provider->getUniqueId();
1362 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1363 switch ( $res->status ) {
1364 case AuthenticationResponse::PASS;
1365 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1366 'user' => $user->getName(),
1367 'creator' => $creator->getName(),
1369 $state['primaryResponse'] = $res;
1371 case AuthenticationResponse::FAIL;
1372 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1373 'user' => $user->getName(),
1374 'creator' => $creator->getName(),
1376 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1377 $session->remove( 'AuthManager::accountCreationState' );
1379 case AuthenticationResponse::REDIRECT;
1380 case AuthenticationResponse::UI;
1381 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1382 'user' => $user->getName(),
1383 'creator' => $creator->getName(),
1385 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1386 $state['continueRequests'] = $res->neededRequests;
1387 $session->setSecret( 'AuthManager::accountCreationState', $state );
1390 throw new \DomainException(
1391 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1396 // Step 2: Primary authentication succeeded, create the User object
1397 // and add the user locally.
1399 if ( $state['userid'] === 0 ) {
1400 $this->logger->info( 'Creating user {user} during account creation', [
1401 'user' => $user->getName(),
1402 'creator' => $creator->getName(),
1404 $status = $user->addToDatabase();
1405 if ( !$status->isOK() ) {
1406 // @codeCoverageIgnoreStart
1407 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1408 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1409 $session->remove( 'AuthManager::accountCreationState' );
1411 // @codeCoverageIgnoreEnd
1413 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1414 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1415 $user->saveSettings();
1416 $state['userid'] = $user->getId();
1418 // Update user count
1419 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1421 // Watch user's userpage and talk page
1422 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1424 // Inform the provider
1425 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1428 if ( $this->config->get( 'NewUserLog' ) ) {
1429 $isAnon = $creator->isAnon();
1430 $logEntry = new \ManualLogEntry(
1432 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1434 $logEntry->setPerformer( $isAnon ? $user : $creator );
1435 $logEntry->setTarget( $user->getUserPage() );
1436 /** @var CreationReasonAuthenticationRequest $req */
1437 $req = AuthenticationRequest::getRequestByClass(
1438 $state['reqs'], CreationReasonAuthenticationRequest::class
1440 $logEntry->setComment( $req ? $req->reason : '' );
1441 $logEntry->setParameters( [
1442 '4::userid' => $user->getId(),
1444 $logid = $logEntry->insert();
1445 $logEntry->publish( $logid );
1449 // Step 3: Iterate over all the secondary authentication providers.
1451 $beginReqs = $state['reqs'];
1453 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1454 if ( !isset( $state['secondary'][$id] ) ) {
1455 // This provider isn't started yet, so we pass it the set
1456 // of reqs from beginAuthentication instead of whatever
1457 // might have been used by a previous provider in line.
1458 $func = 'beginSecondaryAccountCreation';
1459 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1460 } elseif ( !$state['secondary'][$id] ) {
1461 $func = 'continueSecondaryAccountCreation';
1462 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1466 switch ( $res->status ) {
1467 case AuthenticationResponse::PASS;
1468 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1469 'user' => $user->getName(),
1470 'creator' => $creator->getName(),
1473 case AuthenticationResponse::ABSTAIN;
1474 $state['secondary'][$id] = true;
1476 case AuthenticationResponse::REDIRECT;
1477 case AuthenticationResponse::UI;
1478 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1479 'user' => $user->getName(),
1480 'creator' => $creator->getName(),
1482 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1483 $state['secondary'][$id] = false;
1484 $state['continueRequests'] = $res->neededRequests;
1485 $session->setSecret( 'AuthManager::accountCreationState', $state );
1487 case AuthenticationResponse::FAIL;
1488 throw new \DomainException(
1489 get_class( $provider ) . "::{$func}() returned $res->status." .
1490 ' Secondary providers are not allowed to fail account creation, that' .
1491 ' should have been done via testForAccountCreation().'
1493 // @codeCoverageIgnoreStart
1495 throw new \DomainException(
1496 get_class( $provider ) . "::{$func}() returned $res->status"
1498 // @codeCoverageIgnoreEnd
1502 $id = $user->getId();
1503 $name = $user->getName();
1504 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1505 $ret = AuthenticationResponse::newPass( $name );
1506 $ret->loginRequest = $req;
1507 $this->createdAccountAuthenticationRequests[] = $req;
1509 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1510 'user' => $user->getName(),
1511 'creator' => $creator->getName(),
1514 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1515 $session->remove( 'AuthManager::accountCreationState' );
1516 $this->removeAuthenticationSessionData( null );
1518 } catch ( \Exception $ex ) {
1519 $session->remove( 'AuthManager::accountCreationState' );
1525 * Auto-create an account, and log into that account
1527 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1528 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1529 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1530 * the username of a non-existing user from provideSessionInfo(). Calling this method
1531 * explicitly (e.g. from a maintenance script) is also fine.
1533 * @param User $user User to auto-create
1534 * @param string $source What caused the auto-creation? This must be the ID
1535 * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1536 * @param bool $login Whether to also log the user in
1537 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1539 public function autoCreateUser( User $user, $source, $login = true ) {
1540 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1541 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1543 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1546 $username = $user->getName();
1548 // Try the local user from the replica DB
1549 $localId = User::idFromName( $username );
1550 $flags = User::READ_NORMAL;
1552 // Fetch the user ID from the master, so that we don't try to create the user
1553 // when they already exist, due to replication lag
1554 // @codeCoverageIgnoreStart
1555 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1556 $localId = User::idFromName( $username, User::READ_LATEST );
1557 $flags = User::READ_LATEST;
1559 // @codeCoverageIgnoreEnd
1562 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1563 'username' => $username,
1565 $user->setId( $localId );
1566 $user->loadFromId( $flags );
1568 $this->setSessionDataForUser( $user );
1570 $status = Status::newGood();
1571 $status->warning( 'userexists' );
1575 // Wiki is read-only?
1576 if ( wfReadOnly() ) {
1577 $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1578 'username' => $username,
1579 'reason' => wfReadOnlyReason(),
1582 $user->loadFromId();
1583 return Status::newFatal( wfMessage( 'readonlytext', wfReadOnlyReason() ) );
1586 // Check the session, if we tried to create this user already there's
1587 // no point in retrying.
1588 $session = $this->request->getSession();
1589 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1590 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1591 'username' => $username,
1592 'sessionid' => $session->getId(),
1595 $user->loadFromId();
1596 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1597 if ( $reason instanceof StatusValue ) {
1598 return Status::wrap( $reason );
1600 return Status::newFatal( $reason );
1604 // Is the username creatable?
1605 if ( !User::isCreatableName( $username ) ) {
1606 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1607 'username' => $username,
1609 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1611 $user->loadFromId();
1612 return Status::newFatal( 'noname' );
1615 // Is the IP user able to create accounts?
1617 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1618 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1619 'username' => $username,
1620 'ip' => $anon->getName(),
1622 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1623 $session->persist();
1625 $user->loadFromId();
1626 return Status::newFatal( 'authmanager-autocreate-noperm' );
1629 // Avoid account creation races on double submissions
1630 $cache = \ObjectCache::getLocalClusterInstance();
1631 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1633 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1634 'user' => $username,
1637 $user->loadFromId();
1638 return Status::newFatal( 'usernameinprogress' );
1641 // Denied by providers?
1643 'flags' => User::READ_LATEST,
1646 $providers = $this->getPreAuthenticationProviders() +
1647 $this->getPrimaryAuthenticationProviders() +
1648 $this->getSecondaryAuthenticationProviders();
1649 foreach ( $providers as $provider ) {
1650 $status = $provider->testUserForCreation( $user, $source, $options );
1651 if ( !$status->isGood() ) {
1652 $ret = Status::wrap( $status );
1653 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1654 'username' => $username,
1655 'reason' => $ret->getWikiText( null, null, 'en' ),
1657 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1659 $user->loadFromId();
1664 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1665 if ( $cache->get( $backoffKey ) ) {
1666 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1667 'username' => $username,
1670 $user->loadFromId();
1671 return Status::newFatal( 'authmanager-autocreate-exception' );
1674 // Checks passed, create the user...
1675 $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1676 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1677 'username' => $username,
1681 // Ignore warnings about master connections/writes...hard to avoid here
1682 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1683 $old = $trxProfiler->setSilenced( true );
1685 $status = $user->addToDatabase();
1686 if ( !$status->isOK() ) {
1687 // Double-check for a race condition (T70012). We make use of the fact that when
1688 // addToDatabase fails due to the user already existing, the user object gets loaded.
1689 if ( $user->getId() ) {
1690 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1691 'username' => $username,
1694 $this->setSessionDataForUser( $user );
1696 $status = Status::newGood();
1697 $status->warning( 'userexists' );
1699 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1700 'username' => $username,
1701 'msg' => $status->getWikiText( null, null, 'en' )
1704 $user->loadFromId();
1708 } catch ( \Exception $ex ) {
1709 $trxProfiler->setSilenced( $old );
1710 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1711 'username' => $username,
1714 // Do not keep throwing errors for a while
1715 $cache->set( $backoffKey, 1, 600 );
1716 // Bubble up error; which should normally trigger DB rollbacks
1720 $this->setDefaultUserOptions( $user, false );
1722 // Inform the providers
1723 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1725 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1726 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1727 $user->saveSettings();
1729 // Update user count
1730 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1731 // Watch user's userpage and talk page
1732 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1733 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1737 if ( $this->config->get( 'NewUserLog' ) ) {
1738 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1739 $logEntry->setPerformer( $user );
1740 $logEntry->setTarget( $user->getUserPage() );
1741 $logEntry->setComment( '' );
1742 $logEntry->setParameters( [
1743 '4::userid' => $user->getId(),
1745 $logEntry->insert();
1748 $trxProfiler->setSilenced( $old );
1751 $this->setSessionDataForUser( $user );
1754 return Status::newGood();
1760 * @name Account linking
1765 * Determine whether accounts can be linked
1768 public function canLinkAccounts() {
1769 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1770 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1778 * Start an account linking flow
1780 * @param User $user User being linked
1781 * @param AuthenticationRequest[] $reqs
1782 * @param string $returnToUrl Url that REDIRECT responses should eventually
1784 * @return AuthenticationResponse
1786 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1787 $session = $this->request->getSession();
1788 $session->remove( 'AuthManager::accountLinkState' );
1790 if ( !$this->canLinkAccounts() ) {
1791 // Caller should have called canLinkAccounts()
1792 throw new \LogicException( 'Account linking is not possible' );
1795 if ( $user->getId() === 0 ) {
1796 if ( !User::isUsableName( $user->getName() ) ) {
1797 $msg = wfMessage( 'noname' );
1799 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1801 return AuthenticationResponse::newFail( $msg );
1803 foreach ( $reqs as $req ) {
1804 $req->username = $user->getName();
1805 $req->returnToUrl = $returnToUrl;
1808 $this->removeAuthenticationSessionData( null );
1810 $providers = $this->getPreAuthenticationProviders();
1811 foreach ( $providers as $id => $provider ) {
1812 $status = $provider->testForAccountLink( $user );
1813 if ( !$status->isGood() ) {
1814 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1815 'user' => $user->getName(),
1817 $ret = AuthenticationResponse::newFail(
1818 Status::wrap( $status )->getMessage()
1820 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1826 'username' => $user->getName(),
1827 'userid' => $user->getId(),
1828 'returnToUrl' => $returnToUrl,
1830 'continueRequests' => [],
1833 $providers = $this->getPrimaryAuthenticationProviders();
1834 foreach ( $providers as $id => $provider ) {
1835 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1839 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1840 switch ( $res->status ) {
1841 case AuthenticationResponse::PASS;
1842 $this->logger->info( "Account linked to {user} by $id", [
1843 'user' => $user->getName(),
1845 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1848 case AuthenticationResponse::FAIL;
1849 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1850 'user' => $user->getName(),
1852 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1855 case AuthenticationResponse::ABSTAIN;
1859 case AuthenticationResponse::REDIRECT;
1860 case AuthenticationResponse::UI;
1861 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1862 'user' => $user->getName(),
1864 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1865 $state['primary'] = $id;
1866 $state['continueRequests'] = $res->neededRequests;
1867 $session->setSecret( 'AuthManager::accountLinkState', $state );
1868 $session->persist();
1871 // @codeCoverageIgnoreStart
1873 throw new \DomainException(
1874 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1876 // @codeCoverageIgnoreEnd
1880 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1881 'user' => $user->getName(),
1883 $ret = AuthenticationResponse::newFail(
1884 wfMessage( 'authmanager-link-no-primary' )
1886 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1891 * Continue an account linking flow
1892 * @param AuthenticationRequest[] $reqs
1893 * @return AuthenticationResponse
1895 public function continueAccountLink( array $reqs ) {
1896 $session = $this->request->getSession();
1898 if ( !$this->canLinkAccounts() ) {
1899 // Caller should have called canLinkAccounts()
1900 $session->remove( 'AuthManager::accountLinkState' );
1901 throw new \LogicException( 'Account linking is not possible' );
1904 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1905 if ( !is_array( $state ) ) {
1906 return AuthenticationResponse::newFail(
1907 wfMessage( 'authmanager-link-not-in-progress' )
1910 $state['continueRequests'] = [];
1912 // Step 0: Prepare and validate the input
1914 $user = User::newFromName( $state['username'], 'usable' );
1915 if ( !is_object( $user ) ) {
1916 $session->remove( 'AuthManager::accountLinkState' );
1917 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1919 if ( $user->getId() != $state['userid'] ) {
1920 throw new \UnexpectedValueException(
1921 "User \"{$state['username']}\" is valid, but " .
1922 "ID {$user->getId()} != {$state['userid']}!"
1926 foreach ( $reqs as $req ) {
1927 $req->username = $state['username'];
1928 $req->returnToUrl = $state['returnToUrl'];
1931 // Step 1: Call the primary again until it succeeds
1933 $provider = $this->getAuthenticationProvider( $state['primary'] );
1934 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1935 // Configuration changed? Force them to start over.
1936 // @codeCoverageIgnoreStart
1937 $ret = AuthenticationResponse::newFail(
1938 wfMessage( 'authmanager-link-not-in-progress' )
1940 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1941 $session->remove( 'AuthManager::accountLinkState' );
1943 // @codeCoverageIgnoreEnd
1945 $id = $provider->getUniqueId();
1946 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1947 switch ( $res->status ) {
1948 case AuthenticationResponse::PASS;
1949 $this->logger->info( "Account linked to {user} by $id", [
1950 'user' => $user->getName(),
1952 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1953 $session->remove( 'AuthManager::accountLinkState' );
1955 case AuthenticationResponse::FAIL;
1956 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1957 'user' => $user->getName(),
1959 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1960 $session->remove( 'AuthManager::accountLinkState' );
1962 case AuthenticationResponse::REDIRECT;
1963 case AuthenticationResponse::UI;
1964 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1965 'user' => $user->getName(),
1967 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1968 $state['continueRequests'] = $res->neededRequests;
1969 $session->setSecret( 'AuthManager::accountLinkState', $state );
1972 throw new \DomainException(
1973 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1976 } catch ( \Exception $ex ) {
1977 $session->remove( 'AuthManager::accountLinkState' );
1985 * @name Information methods
1990 * Return the applicable list of AuthenticationRequests
1992 * Possible values for $action:
1993 * - ACTION_LOGIN: Valid for passing to beginAuthentication
1994 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1995 * - ACTION_CREATE: Valid for passing to beginAccountCreation
1996 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1997 * - ACTION_LINK: Valid for passing to beginAccountLink
1998 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1999 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
2000 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
2001 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2003 * @param string $action One of the AuthManager::ACTION_* constants
2004 * @param User|null $user User being acted on, instead of the current user.
2005 * @return AuthenticationRequest[]
2007 public function getAuthenticationRequests( $action, User $user = null ) {
2009 $providerAction = $action;
2011 // Figure out which providers to query
2012 switch ( $action ) {
2013 case self::ACTION_LOGIN:
2014 case self::ACTION_CREATE:
2015 $providers = $this->getPreAuthenticationProviders() +
2016 $this->getPrimaryAuthenticationProviders() +
2017 $this->getSecondaryAuthenticationProviders();
2020 case self::ACTION_LOGIN_CONTINUE:
2021 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2022 return is_array( $state ) ? $state['continueRequests'] : [];
2024 case self::ACTION_CREATE_CONTINUE:
2025 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2026 return is_array( $state ) ? $state['continueRequests'] : [];
2028 case self::ACTION_LINK:
2029 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2030 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2034 case self::ACTION_UNLINK:
2035 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2036 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
2039 // To providers, unlink and remove are identical.
2040 $providerAction = self::ACTION_REMOVE;
2043 case self::ACTION_LINK_CONTINUE:
2044 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2045 return is_array( $state ) ? $state['continueRequests'] : [];
2047 case self::ACTION_CHANGE:
2048 case self::ACTION_REMOVE:
2049 $providers = $this->getPrimaryAuthenticationProviders() +
2050 $this->getSecondaryAuthenticationProviders();
2053 // @codeCoverageIgnoreStart
2055 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2057 // @codeCoverageIgnoreEnd
2059 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2063 * Internal request lookup for self::getAuthenticationRequests
2065 * @param string $providerAction Action to pass to providers
2066 * @param array $options Options to pass to providers
2067 * @param AuthenticationProvider[] $providers
2068 * @param User|null $user
2069 * @return AuthenticationRequest[]
2071 private function getAuthenticationRequestsInternal(
2072 $providerAction, array $options, array $providers, User $user = null
2074 $user = $user ?: \RequestContext::getMain()->getUser();
2075 $options['username'] = $user->isAnon() ? null : $user->getName();
2077 // Query them and merge results
2079 foreach ( $providers as $provider ) {
2080 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2081 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2082 $id = $req->getUniqueId();
2084 // If a required request if from a Primary, mark it as "primary-required" instead
2086 if ( $req->required ) {
2087 $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2092 !isset( $reqs[$id] )
2093 || $req->required === AuthenticationRequest::REQUIRED
2094 || $reqs[$id] === AuthenticationRequest::OPTIONAL
2101 // AuthManager has its own req for some actions
2102 switch ( $providerAction ) {
2103 case self::ACTION_LOGIN:
2104 $reqs[] = new RememberMeAuthenticationRequest;
2107 case self::ACTION_CREATE:
2108 $reqs[] = new UsernameAuthenticationRequest;
2109 $reqs[] = new UserDataAuthenticationRequest;
2110 if ( $options['username'] !== null ) {
2111 $reqs[] = new CreationReasonAuthenticationRequest;
2112 $options['username'] = null; // Don't fill in the username below
2117 // Fill in reqs data
2118 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2120 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2121 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2122 $reqs = array_filter( $reqs, function ( $req ) {
2123 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2127 return array_values( $reqs );
2131 * Set values in an array of requests
2132 * @param AuthenticationRequest[] &$reqs
2133 * @param string $action
2134 * @param string|null $username
2135 * @param bool $forceAction
2137 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2138 foreach ( $reqs as $req ) {
2139 if ( !$req->action || $forceAction ) {
2140 $req->action = $action;
2142 if ( $req->username === null ) {
2143 $req->username = $username;
2149 * Determine whether a username exists
2150 * @param string $username
2151 * @param int $flags Bitfield of User:READ_* constants
2154 public function userExists( $username, $flags = User::READ_NORMAL ) {
2155 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2156 if ( $provider->testUserExists( $username, $flags ) ) {
2165 * Determine whether a user property should be allowed to be changed.
2167 * Supported properties are:
2172 * @param string $property
2175 public function allowsPropertyChange( $property ) {
2176 $providers = $this->getPrimaryAuthenticationProviders() +
2177 $this->getSecondaryAuthenticationProviders();
2178 foreach ( $providers as $provider ) {
2179 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2187 * Get a provider by ID
2188 * @note This is public so extensions can check whether their own provider
2189 * is installed and so they can read its configuration if necessary.
2190 * Other uses are not recommended.
2192 * @return AuthenticationProvider|null
2194 public function getAuthenticationProvider( $id ) {
2196 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2197 return $this->allAuthenticationProviders[$id];
2200 // Slow version: instantiate each kind and check
2201 $providers = $this->getPrimaryAuthenticationProviders();
2202 if ( isset( $providers[$id] ) ) {
2203 return $providers[$id];
2205 $providers = $this->getSecondaryAuthenticationProviders();
2206 if ( isset( $providers[$id] ) ) {
2207 return $providers[$id];
2209 $providers = $this->getPreAuthenticationProviders();
2210 if ( isset( $providers[$id] ) ) {
2211 return $providers[$id];
2220 * @name Internal methods
2225 * Store authentication in the current session
2226 * @protected For use by AuthenticationProviders
2227 * @param string $key
2228 * @param mixed $data Must be serializable
2230 public function setAuthenticationSessionData( $key, $data ) {
2231 $session = $this->request->getSession();
2232 $arr = $session->getSecret( 'authData' );
2233 if ( !is_array( $arr ) ) {
2237 $session->setSecret( 'authData', $arr );
2241 * Fetch authentication data from the current session
2242 * @protected For use by AuthenticationProviders
2243 * @param string $key
2244 * @param mixed $default
2247 public function getAuthenticationSessionData( $key, $default = null ) {
2248 $arr = $this->request->getSession()->getSecret( 'authData' );
2249 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2257 * Remove authentication data
2258 * @protected For use by AuthenticationProviders
2259 * @param string|null $key If null, all data is removed
2261 public function removeAuthenticationSessionData( $key ) {
2262 $session = $this->request->getSession();
2263 if ( $key === null ) {
2264 $session->remove( 'authData' );
2266 $arr = $session->getSecret( 'authData' );
2267 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2268 unset( $arr[$key] );
2269 $session->setSecret( 'authData', $arr );
2275 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2276 * @param string $class
2277 * @param array[] $specs
2278 * @return AuthenticationProvider[]
2280 protected function providerArrayFromSpecs( $class, array $specs ) {
2282 foreach ( $specs as &$spec ) {
2283 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2286 usort( $specs, function ( $a, $b ) {
2287 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2288 ?: $a['sort2'] - $b['sort2'];
2292 foreach ( $specs as $spec ) {
2293 $provider = \ObjectFactory::getObjectFromSpec( $spec );
2294 if ( !$provider instanceof $class ) {
2295 throw new \RuntimeException(
2296 "Expected instance of $class, got " . get_class( $provider )
2299 $provider->setLogger( $this->logger );
2300 $provider->setManager( $this );
2301 $provider->setConfig( $this->config );
2302 $id = $provider->getUniqueId();
2303 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2304 throw new \RuntimeException(
2305 "Duplicate specifications for id $id (classes " .
2306 get_class( $provider ) . ' and ' .
2307 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2310 $this->allAuthenticationProviders[$id] = $provider;
2311 $ret[$id] = $provider;
2317 * Get the configuration
2320 private function getConfiguration() {
2321 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2325 * Get the list of PreAuthenticationProviders
2326 * @return PreAuthenticationProvider[]
2328 protected function getPreAuthenticationProviders() {
2329 if ( $this->preAuthenticationProviders === null ) {
2330 $conf = $this->getConfiguration();
2331 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2332 PreAuthenticationProvider::class, $conf['preauth']
2335 return $this->preAuthenticationProviders;
2339 * Get the list of PrimaryAuthenticationProviders
2340 * @return PrimaryAuthenticationProvider[]
2342 protected function getPrimaryAuthenticationProviders() {
2343 if ( $this->primaryAuthenticationProviders === null ) {
2344 $conf = $this->getConfiguration();
2345 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2346 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2349 return $this->primaryAuthenticationProviders;
2353 * Get the list of SecondaryAuthenticationProviders
2354 * @return SecondaryAuthenticationProvider[]
2356 protected function getSecondaryAuthenticationProviders() {
2357 if ( $this->secondaryAuthenticationProviders === null ) {
2358 $conf = $this->getConfiguration();
2359 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2360 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2363 return $this->secondaryAuthenticationProviders;
2369 * @param bool|null $remember
2371 private function setSessionDataForUser( $user, $remember = null ) {
2372 $session = $this->request->getSession();
2373 $delay = $session->delaySave();
2375 $session->resetId();
2376 $session->resetAllTokens();
2377 if ( $session->canSetUser() ) {
2378 $session->setUser( $user );
2380 if ( $remember !== null ) {
2381 $session->setRememberUser( $remember );
2383 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2384 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2385 $session->persist();
2387 \Wikimedia\ScopedCallback::consume( $delay );
2389 \Hooks::run( 'UserLoggedIn', [ $user ] );
2394 * @param bool $useContextLang Use 'uselang' to set the user's language
2396 private function setDefaultUserOptions( User $user, $useContextLang ) {
2401 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2402 $user->setOption( 'language', $lang->getPreferredVariant() );
2404 if ( $wgContLang->hasVariants() ) {
2405 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2410 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2411 * @param string $method
2412 * @param array $args
2414 private function callMethodOnProviders( $which, $method, array $args ) {
2417 $providers += $this->getPreAuthenticationProviders();
2420 $providers += $this->getPrimaryAuthenticationProviders();
2423 $providers += $this->getSecondaryAuthenticationProviders();
2425 foreach ( $providers as $provider ) {
2426 call_user_func_array( [ $provider, $method ], $args );
2431 * Reset the internal caching for unit testing
2432 * @protected Unit tests only
2434 public static function resetCache() {
2435 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2436 // @codeCoverageIgnoreStart
2437 throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2438 // @codeCoverageIgnoreEnd
2441 self::$instance = null;
2449 * For really cool vim folding this needs to be at the end:
2450 * vim: foldmarker=@{,@} foldmethod=marker