3 * Authentication (and possibly Authorization in the future) system entry point
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 namespace MediaWiki\Auth
;
27 use Psr\Log\LoggerAwareInterface
;
28 use Psr\Log\LoggerInterface
;
35 * This serves as the entry point to the authentication system.
37 * In the future, it may also serve as the entry point to the authorization
40 * If you are looking at this because you are working on an extension that creates its own
41 * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
42 * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
43 * or the createaccount API. Trying to call this class directly will very likely end up in
44 * security vulnerabilities or broken UX in edge cases.
46 * If you are working on an extension that needs to integrate with the authentication system
47 * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
48 * need to write an AuthenticationProvider.
50 * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
51 * you are looking for. If you want to change user data, use User::changeAuthenticationData().
52 * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
53 * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
54 * responsibility to ensure that the user can authenticate somehow (see especially
55 * PrimaryAuthenticationProvider::autoCreatedAccount()).
56 * If you are writing code that is not associated with such a provider and needs to create accounts
57 * programmatically for real users, you should rethink your architecture. There is no good way to
58 * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
59 * cannot provide any means for users to access the accounts it would create.
61 * The two main control flows when using this class are as follows:
62 * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
63 * the requests with data (by using them to build a HTMLForm and have the user fill it, or by
64 * exposing a form specification via the API, so that the client can build it), and pass them to
65 * the appropriate begin* method. That will return either a success/failure response, or more
66 * requests to fill (either by building a form or by redirecting the user to some external
67 * provider which will send the data back), in which case they need to be submitted to the
68 * appropriate continue* method and that step has to be repeated until the response is a success
69 * or failure response. AuthManager will use the session to maintain internal state during the
71 * * Code doing an authentication data change will call getAuthenticationRequests(), select
72 * a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
73 * changeAuthenticationData(). If the data change is user-initiated, the whole process needs
74 * to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
79 * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
81 class AuthManager
implements LoggerAwareInterface
{
82 /** Log in with an existing (not necessarily local) user */
83 const ACTION_LOGIN
= 'login';
84 /** Continue a login process that was interrupted by the need for user input or communication
85 * with an external provider */
86 const ACTION_LOGIN_CONTINUE
= 'login-continue';
87 /** Create a new user */
88 const ACTION_CREATE
= 'create';
89 /** Continue a user creation process that was interrupted by the need for user input or
90 * communication with an external provider */
91 const ACTION_CREATE_CONTINUE
= 'create-continue';
92 /** Link an existing user to a third-party account */
93 const ACTION_LINK
= 'link';
94 /** Continue a user linking process that was interrupted by the need for user input or
95 * communication with an external provider */
96 const ACTION_LINK_CONTINUE
= 'link-continue';
97 /** Change a user's credentials */
98 const ACTION_CHANGE
= 'change';
99 /** Remove a user's credentials */
100 const ACTION_REMOVE
= 'remove';
101 /** Like ACTION_REMOVE but for linking providers only */
102 const ACTION_UNLINK
= 'unlink';
104 /** Security-sensitive operations are ok. */
106 /** Security-sensitive operations should re-authenticate. */
107 const SEC_REAUTH
= 'reauth';
108 /** Security-sensitive should not be performed. */
109 const SEC_FAIL
= 'fail';
111 /** Auto-creation is due to SessionManager */
112 const AUTOCREATE_SOURCE_SESSION
= \MediaWiki\Session\SessionManager
::class;
114 /** @var AuthManager|null */
115 private static $instance = null;
117 /** @var WebRequest */
123 /** @var LoggerInterface */
126 /** @var AuthenticationProvider[] */
127 private $allAuthenticationProviders = [];
129 /** @var PreAuthenticationProvider[] */
130 private $preAuthenticationProviders = null;
132 /** @var PrimaryAuthenticationProvider[] */
133 private $primaryAuthenticationProviders = null;
135 /** @var SecondaryAuthenticationProvider[] */
136 private $secondaryAuthenticationProviders = null;
138 /** @var CreatedAccountAuthenticationRequest[] */
139 private $createdAccountAuthenticationRequests = [];
142 * Get the global AuthManager
143 * @return AuthManager
145 public static function singleton() {
146 if ( self
::$instance === null ) {
147 self
::$instance = new self(
148 \RequestContext
::getMain()->getRequest(),
149 \ConfigFactory
::getDefaultInstance()->makeConfig( 'main' )
152 return self
::$instance;
156 * @param WebRequest $request
157 * @param Config $config
159 public function __construct( WebRequest
$request, Config
$config ) {
160 $this->request
= $request;
161 $this->config
= $config;
162 $this->setLogger( \MediaWiki\Logger\LoggerFactory
::getInstance( 'authentication' ) );
166 * @param LoggerInterface $logger
168 public function setLogger( LoggerInterface
$logger ) {
169 $this->logger
= $logger;
175 public function getRequest() {
176 return $this->request
;
180 * Force certain PrimaryAuthenticationProviders
181 * @deprecated For backwards compatibility only
182 * @param PrimaryAuthenticationProvider[] $providers
185 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
186 $this->logger
->warning( "Overriding AuthManager primary authn because $why" );
188 if ( $this->primaryAuthenticationProviders
!== null ) {
189 $this->logger
->warning(
190 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
193 $this->allAuthenticationProviders
= array_diff_key(
194 $this->allAuthenticationProviders
,
195 $this->primaryAuthenticationProviders
197 $session = $this->request
->getSession();
198 $session->remove( 'AuthManager::authnState' );
199 $session->remove( 'AuthManager::accountCreationState' );
200 $session->remove( 'AuthManager::accountLinkState' );
201 $this->createdAccountAuthenticationRequests
= [];
204 $this->primaryAuthenticationProviders
= [];
205 foreach ( $providers as $provider ) {
206 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
207 throw new \
RuntimeException(
208 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
209 get_class( $provider )
212 $provider->setLogger( $this->logger
);
213 $provider->setManager( $this );
214 $provider->setConfig( $this->config
);
215 $id = $provider->getUniqueId();
216 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
217 throw new \
RuntimeException(
218 "Duplicate specifications for id $id (classes " .
219 get_class( $provider ) . ' and ' .
220 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
223 $this->allAuthenticationProviders
[$id] = $provider;
224 $this->primaryAuthenticationProviders
[$id] = $provider;
229 * Call a legacy AuthPlugin method, if necessary
230 * @codeCoverageIgnore
231 * @deprecated For backwards compatibility only, should be avoided in new code
232 * @param string $method AuthPlugin method to call
233 * @param array $params Parameters to pass
234 * @param mixed $return Return value if AuthPlugin wasn't called
235 * @return mixed Return value from the AuthPlugin method, or $return
237 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
240 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin
) {
241 return call_user_func_array( [ $wgAuth, $method ], $params );
248 * @name Authentication
253 * Indicate whether user authentication is possible
255 * It may not be if the session is provided by something like OAuth
256 * for which each individual request includes authentication data.
260 public function canAuthenticateNow() {
261 return $this->request
->getSession()->canSetUser();
265 * Start an authentication flow
267 * In addition to the AuthenticationRequests returned by
268 * $this->getAuthenticationRequests(), a client might include a
269 * CreateFromLoginAuthenticationRequest from a previous login attempt to
272 * Instead of the AuthenticationRequests returned by
273 * $this->getAuthenticationRequests(), a client might pass a
274 * CreatedAccountAuthenticationRequest from an account creation that just
275 * succeeded to log in to the just-created account.
277 * @param AuthenticationRequest[] $reqs
278 * @param string $returnToUrl Url that REDIRECT responses should eventually
280 * @return AuthenticationResponse See self::continueAuthentication()
282 public function beginAuthentication( array $reqs, $returnToUrl ) {
283 $session = $this->request
->getSession();
284 if ( !$session->canSetUser() ) {
285 // Caller should have called canAuthenticateNow()
286 $session->remove( 'AuthManager::authnState' );
287 throw new \
LogicException( 'Authentication is not possible now' );
290 $guessUserName = null;
291 foreach ( $reqs as $req ) {
292 $req->returnToUrl
= $returnToUrl;
293 // @codeCoverageIgnoreStart
294 if ( $req->username
!== null && $req->username
!== '' ) {
295 if ( $guessUserName === null ) {
296 $guessUserName = $req->username
;
297 } elseif ( $guessUserName !== $req->username
) {
298 $guessUserName = null;
302 // @codeCoverageIgnoreEnd
305 // Check for special-case login of a just-created account
306 $req = AuthenticationRequest
::getRequestByClass(
307 $reqs, CreatedAccountAuthenticationRequest
::class
310 if ( !in_array( $req, $this->createdAccountAuthenticationRequests
, true ) ) {
311 throw new \
LogicException(
312 'CreatedAccountAuthenticationRequests are only valid on ' .
313 'the same AuthManager that created the account'
317 $user = User
::newFromName( $req->username
);
318 // @codeCoverageIgnoreStart
320 throw new \
UnexpectedValueException(
321 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
323 } elseif ( $user->getId() != $req->id
) {
324 throw new \
UnexpectedValueException(
325 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
328 // @codeCoverageIgnoreEnd
330 $this->logger
->info( 'Logging in {user} after account creation', [
331 'user' => $user->getName(),
333 $ret = AuthenticationResponse
::newPass( $user->getName() );
334 $this->setSessionDataForUser( $user );
335 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
336 $session->remove( 'AuthManager::authnState' );
337 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
341 $this->removeAuthenticationSessionData( null );
343 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
344 $status = $provider->testForAuthentication( $reqs );
345 if ( !$status->isGood() ) {
346 $this->logger
->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
347 $ret = AuthenticationResponse
::newFail(
348 Status
::wrap( $status )->getMessage()
350 $this->callMethodOnProviders( 7, 'postAuthentication',
351 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
353 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
360 'returnToUrl' => $returnToUrl,
361 'guessUserName' => $guessUserName,
363 'primaryResponse' => null,
366 'continueRequests' => [],
369 // Preserve state from a previous failed login
370 $req = AuthenticationRequest
::getRequestByClass(
371 $reqs, CreateFromLoginAuthenticationRequest
::class
374 $state['maybeLink'] = $req->maybeLink
;
377 $session = $this->request
->getSession();
378 $session->setSecret( 'AuthManager::authnState', $state );
381 return $this->continueAuthentication( $reqs );
385 * Continue an authentication flow
387 * Return values are interpreted as follows:
388 * - status FAIL: Authentication failed. If $response->createRequest is
389 * set, that may be passed to self::beginAuthentication() or to
390 * self::beginAccountCreation() to preserve state.
391 * - status REDIRECT: The client should be redirected to the contained URL,
392 * new AuthenticationRequests should be made (if any), then
393 * AuthManager::continueAuthentication() should be called.
394 * - status UI: The client should be presented with a user interface for
395 * the fields in the specified AuthenticationRequests, then new
396 * AuthenticationRequests should be made, then
397 * AuthManager::continueAuthentication() should be called.
398 * - status RESTART: The user logged in successfully with a third-party
399 * service, but the third-party credentials aren't attached to any local
400 * account. This could be treated as a UI or a FAIL.
401 * - status PASS: Authentication was successful.
403 * @param AuthenticationRequest[] $reqs
404 * @return AuthenticationResponse
406 public function continueAuthentication( array $reqs ) {
407 $session = $this->request
->getSession();
409 if ( !$session->canSetUser() ) {
410 // Caller should have called canAuthenticateNow()
411 // @codeCoverageIgnoreStart
412 throw new \
LogicException( 'Authentication is not possible now' );
413 // @codeCoverageIgnoreEnd
416 $state = $session->getSecret( 'AuthManager::authnState' );
417 if ( !is_array( $state ) ) {
418 return AuthenticationResponse
::newFail(
419 wfMessage( 'authmanager-authn-not-in-progress' )
422 $state['continueRequests'] = [];
424 $guessUserName = $state['guessUserName'];
426 foreach ( $reqs as $req ) {
427 $req->returnToUrl
= $state['returnToUrl'];
430 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
432 if ( $state['primary'] === null ) {
433 // We haven't picked a PrimaryAuthenticationProvider yet
434 // @codeCoverageIgnoreStart
435 $guessUserName = null;
436 foreach ( $reqs as $req ) {
437 if ( $req->username
!== null && $req->username
!== '' ) {
438 if ( $guessUserName === null ) {
439 $guessUserName = $req->username
;
440 } elseif ( $guessUserName !== $req->username
) {
441 $guessUserName = null;
446 $state['guessUserName'] = $guessUserName;
447 // @codeCoverageIgnoreEnd
448 $state['reqs'] = $reqs;
450 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
451 $res = $provider->beginPrimaryAuthentication( $reqs );
452 switch ( $res->status
) {
453 case AuthenticationResponse
::PASS
;
454 $state['primary'] = $id;
455 $state['primaryResponse'] = $res;
456 $this->logger
->debug( "Primary login with $id succeeded" );
458 case AuthenticationResponse
::FAIL
;
459 $this->logger
->debug( "Login failed in primary authentication by $id" );
460 if ( $res->createRequest ||
$state['maybeLink'] ) {
461 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
462 $res->createRequest
, $state['maybeLink']
465 $this->callMethodOnProviders( 7, 'postAuthentication',
466 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
468 $session->remove( 'AuthManager::authnState' );
469 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
471 case AuthenticationResponse
::ABSTAIN
;
474 case AuthenticationResponse
::REDIRECT
;
475 case AuthenticationResponse
::UI
;
476 $this->logger
->debug( "Primary login with $id returned $res->status" );
477 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
478 $state['primary'] = $id;
479 $state['continueRequests'] = $res->neededRequests
;
480 $session->setSecret( 'AuthManager::authnState', $state );
483 // @codeCoverageIgnoreStart
485 throw new \
DomainException(
486 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
488 // @codeCoverageIgnoreEnd
491 if ( $state['primary'] === null ) {
492 $this->logger
->debug( 'Login failed in primary authentication because no provider accepted' );
493 $ret = AuthenticationResponse
::newFail(
494 wfMessage( 'authmanager-authn-no-primary' )
496 $this->callMethodOnProviders( 7, 'postAuthentication',
497 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
499 $session->remove( 'AuthManager::authnState' );
502 } elseif ( $state['primaryResponse'] === null ) {
503 $provider = $this->getAuthenticationProvider( $state['primary'] );
504 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
505 // Configuration changed? Force them to start over.
506 // @codeCoverageIgnoreStart
507 $ret = AuthenticationResponse
::newFail(
508 wfMessage( 'authmanager-authn-not-in-progress' )
510 $this->callMethodOnProviders( 7, 'postAuthentication',
511 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
513 $session->remove( 'AuthManager::authnState' );
515 // @codeCoverageIgnoreEnd
517 $id = $provider->getUniqueId();
518 $res = $provider->continuePrimaryAuthentication( $reqs );
519 switch ( $res->status
) {
520 case AuthenticationResponse
::PASS
;
521 $state['primaryResponse'] = $res;
522 $this->logger
->debug( "Primary login with $id succeeded" );
524 case AuthenticationResponse
::FAIL
;
525 $this->logger
->debug( "Login failed in primary authentication by $id" );
526 if ( $res->createRequest ||
$state['maybeLink'] ) {
527 $res->createRequest
= new CreateFromLoginAuthenticationRequest(
528 $res->createRequest
, $state['maybeLink']
531 $this->callMethodOnProviders( 7, 'postAuthentication',
532 [ User
::newFromName( $guessUserName ) ?
: null, $res ]
534 $session->remove( 'AuthManager::authnState' );
535 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
537 case AuthenticationResponse
::REDIRECT
;
538 case AuthenticationResponse
::UI
;
539 $this->logger
->debug( "Primary login with $id returned $res->status" );
540 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $guessUserName );
541 $state['continueRequests'] = $res->neededRequests
;
542 $session->setSecret( 'AuthManager::authnState', $state );
545 throw new \
DomainException(
546 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
551 $res = $state['primaryResponse'];
552 if ( $res->username
=== null ) {
553 $provider = $this->getAuthenticationProvider( $state['primary'] );
554 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
555 // Configuration changed? Force them to start over.
556 // @codeCoverageIgnoreStart
557 $ret = AuthenticationResponse
::newFail(
558 wfMessage( 'authmanager-authn-not-in-progress' )
560 $this->callMethodOnProviders( 7, 'postAuthentication',
561 [ User
::newFromName( $guessUserName ) ?
: null, $ret ]
563 $session->remove( 'AuthManager::authnState' );
565 // @codeCoverageIgnoreEnd
568 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
&&
570 // don't confuse the user with an incorrect message if linking is disabled
571 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider
::class )
573 $state['maybeLink'][$res->linkRequest
->getUniqueId()] = $res->linkRequest
;
574 $msg = 'authmanager-authn-no-local-user-link';
576 $msg = 'authmanager-authn-no-local-user';
578 $this->logger
->debug(
579 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
581 $ret = AuthenticationResponse
::newRestart( wfMessage( $msg ) );
582 $ret->neededRequests
= $this->getAuthenticationRequestsInternal(
585 $this->getPrimaryAuthenticationProviders() +
$this->getSecondaryAuthenticationProviders()
587 if ( $res->createRequest ||
$state['maybeLink'] ) {
588 $ret->createRequest
= new CreateFromLoginAuthenticationRequest(
589 $res->createRequest
, $state['maybeLink']
591 $ret->neededRequests
[] = $ret->createRequest
;
593 $this->fillRequests( $ret->neededRequests
, self
::ACTION_LOGIN
, null, true );
594 $session->setSecret( 'AuthManager::authnState', [
595 'reqs' => [], // Will be filled in later
597 'primaryResponse' => null,
599 'continueRequests' => $ret->neededRequests
,
604 // Step 2: Primary authentication succeeded, create the User object
605 // (and add the user locally if necessary)
607 $user = User
::newFromName( $res->username
, 'usable' );
609 $provider = $this->getAuthenticationProvider( $state['primary'] );
610 throw new \
DomainException(
611 get_class( $provider ) . " returned an invalid username: {$res->username}"
614 if ( $user->getId() === 0 ) {
615 // User doesn't exist locally. Create it.
616 $this->logger
->info( 'Auto-creating {user} on login', [
617 'user' => $user->getName(),
619 $status = $this->autoCreateUser( $user, $state['primary'], false );
620 if ( !$status->isGood() ) {
621 $ret = AuthenticationResponse
::newFail(
622 Status
::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
624 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
625 $session->remove( 'AuthManager::authnState' );
626 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
631 // Step 3: Iterate over all the secondary authentication providers.
633 $beginReqs = $state['reqs'];
635 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
636 if ( !isset( $state['secondary'][$id] ) ) {
637 // This provider isn't started yet, so we pass it the set
638 // of reqs from beginAuthentication instead of whatever
639 // might have been used by a previous provider in line.
640 $func = 'beginSecondaryAuthentication';
641 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
642 } elseif ( !$state['secondary'][$id] ) {
643 $func = 'continueSecondaryAuthentication';
644 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
648 switch ( $res->status
) {
649 case AuthenticationResponse
::PASS
;
650 $this->logger
->debug( "Secondary login with $id succeeded" );
652 case AuthenticationResponse
::ABSTAIN
;
653 $state['secondary'][$id] = true;
655 case AuthenticationResponse
::FAIL
;
656 $this->logger
->debug( "Login failed in secondary authentication by $id" );
657 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
658 $session->remove( 'AuthManager::authnState' );
659 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
661 case AuthenticationResponse
::REDIRECT
;
662 case AuthenticationResponse
::UI
;
663 $this->logger
->debug( "Secondary login with $id returned " . $res->status
);
664 $this->fillRequests( $res->neededRequests
, self
::ACTION_LOGIN
, $user->getName() );
665 $state['secondary'][$id] = false;
666 $state['continueRequests'] = $res->neededRequests
;
667 $session->setSecret( 'AuthManager::authnState', $state );
670 // @codeCoverageIgnoreStart
672 throw new \
DomainException(
673 get_class( $provider ) . "::{$func}() returned $res->status"
675 // @codeCoverageIgnoreEnd
679 // Step 4: Authentication complete! Set the user in the session and
682 $this->logger
->info( 'Login for {user} succeeded', [
683 'user' => $user->getName(),
685 /** @var RememberMeAuthenticationRequest $req */
686 $req = AuthenticationRequest
::getRequestByClass(
687 $beginReqs, RememberMeAuthenticationRequest
::class
689 $this->setSessionDataForUser( $user, $req && $req->rememberMe
);
690 $ret = AuthenticationResponse
::newPass( $user->getName() );
691 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
692 $session->remove( 'AuthManager::authnState' );
693 $this->removeAuthenticationSessionData( null );
694 \Hooks
::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
696 } catch ( \Exception
$ex ) {
697 $session->remove( 'AuthManager::authnState' );
703 * Whether security-sensitive operations should proceed.
705 * A "security-sensitive operation" is something like a password or email
706 * change, that would normally have a "reenter your password to confirm"
707 * box if we only supported password-based authentication.
709 * @param string $operation Operation being checked. This should be a
710 * message-key-like string such as 'change-password' or 'change-email'.
711 * @return string One of the SEC_* constants.
713 public function securitySensitiveOperationStatus( $operation ) {
714 $status = self
::SEC_OK
;
716 $this->logger
->debug( __METHOD__
. ": Checking $operation" );
718 $session = $this->request
->getSession();
719 $aId = $session->getUser()->getId();
721 // User isn't authenticated. DWIM?
722 $status = $this->canAuthenticateNow() ? self
::SEC_REAUTH
: self
::SEC_FAIL
;
723 $this->logger
->info( __METHOD__
. ": Not logged in! $operation is $status" );
727 if ( $session->canSetUser() ) {
728 $id = $session->get( 'AuthManager:lastAuthId' );
729 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
730 if ( $id !== $aId ||
$last === null ) {
731 $timeSinceLogin = PHP_INT_MAX
; // Forever ago
733 $timeSinceLogin = max( 0, time() - $last );
736 $thresholds = $this->config
->get( 'ReauthenticateTime' );
737 if ( isset( $thresholds[$operation] ) ) {
738 $threshold = $thresholds[$operation];
739 } elseif ( isset( $thresholds['default'] ) ) {
740 $threshold = $thresholds['default'];
742 throw new \
UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
745 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
746 $status = self
::SEC_REAUTH
;
749 $timeSinceLogin = -1;
751 $pass = $this->config
->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
752 if ( isset( $pass[$operation] ) ) {
753 $status = $pass[$operation] ? self
::SEC_OK
: self
::SEC_FAIL
;
754 } elseif ( isset( $pass['default'] ) ) {
755 $status = $pass['default'] ? self
::SEC_OK
: self
::SEC_FAIL
;
757 throw new \
UnexpectedValueException(
758 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
763 \Hooks
::run( 'SecuritySensitiveOperationStatus', [
764 &$status, $operation, $session, $timeSinceLogin
767 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
768 if ( !$this->canAuthenticateNow() && $status === self
::SEC_REAUTH
) {
769 $status = self
::SEC_FAIL
;
772 $this->logger
->info( __METHOD__
. ": $operation is $status" );
778 * Determine whether a username can authenticate
780 * This is mainly for internal purposes and only takes authentication data into account,
781 * not things like blocks that can change without the authentication system being aware.
783 * @param string $username MediaWiki username
786 public function userCanAuthenticate( $username ) {
787 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
788 if ( $provider->testUserCanAuthenticate( $username ) ) {
796 * Provide normalized versions of the username for security checks
798 * Since different providers can normalize the input in different ways,
799 * this returns an array of all the different ways the name might be
800 * normalized for authentication.
802 * The returned strings should not be revealed to the user, as that might
803 * leak private information (e.g. an email address might be normalized to a
806 * @param string $username
809 public function normalizeUsername( $username ) {
811 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
812 $normalized = $provider->providerNormalizeUsername( $username );
813 if ( $normalized !== null ) {
814 $ret[$normalized] = true;
817 return array_keys( $ret );
823 * @name Authentication data changing
828 * Revoke any authentication credentials for a user
830 * After this, the user should no longer be able to log in.
832 * @param string $username
834 public function revokeAccessForUser( $username ) {
835 $this->logger
->info( 'Revoking access for {user}', [
838 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
842 * Validate a change of authentication data (e.g. passwords)
843 * @param AuthenticationRequest $req
844 * @param bool $checkData If false, $req hasn't been loaded from the
845 * submission so checks on user-submitted fields should be skipped. $req->username is
846 * considered user-submitted for this purpose, even if it cannot be changed via
847 * $req->loadFromSubmission.
850 public function allowsAuthenticationDataChange( AuthenticationRequest
$req, $checkData = true ) {
852 $providers = $this->getPrimaryAuthenticationProviders() +
853 $this->getSecondaryAuthenticationProviders();
854 foreach ( $providers as $provider ) {
855 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
856 if ( !$status->isGood() ) {
857 return Status
::wrap( $status );
859 $any = $any ||
$status->value
!== 'ignored';
862 $status = Status
::newGood( 'ignored' );
863 $status->warning( 'authmanager-change-not-supported' );
866 return Status
::newGood();
870 * Change authentication data (e.g. passwords)
872 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
873 * result in a successful login in the future.
875 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
876 * no longer result in a successful login.
878 * This method should only be called if allowsAuthenticationDataChange( $req, true )
881 * @param AuthenticationRequest $req
883 public function changeAuthenticationData( AuthenticationRequest
$req ) {
884 $this->logger
->info( 'Changing authentication data for {user} class {what}', [
885 'user' => is_string( $req->username
) ?
$req->username
: '<no name>',
886 'what' => get_class( $req ),
889 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
891 // When the main account's authentication data is changed, invalidate
892 // all BotPasswords too.
893 \BotPassword
::invalidateAllPasswordsForUser( $req->username
);
899 * @name Account creation
904 * Determine whether accounts can be created
907 public function canCreateAccounts() {
908 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
909 switch ( $provider->accountCreationType() ) {
910 case PrimaryAuthenticationProvider
::TYPE_CREATE
:
911 case PrimaryAuthenticationProvider
::TYPE_LINK
:
919 * Determine whether a particular account can be created
920 * @param string $username MediaWiki username
921 * @param array $options
922 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
923 * - creating: (bool) For internal use only. Never specify this.
926 public function canCreateAccount( $username, $options = [] ) {
928 if ( is_int( $options ) ) {
929 $options = [ 'flags' => $options ];
932 'flags' => User
::READ_NORMAL
,
935 $flags = $options['flags'];
937 if ( !$this->canCreateAccounts() ) {
938 return Status
::newFatal( 'authmanager-create-disabled' );
941 if ( $this->userExists( $username, $flags ) ) {
942 return Status
::newFatal( 'userexists' );
945 $user = User
::newFromName( $username, 'creatable' );
946 if ( !is_object( $user ) ) {
947 return Status
::newFatal( 'noname' );
949 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
950 if ( $user->getId() !== 0 ) {
951 return Status
::newFatal( 'userexists' );
955 // Denied by providers?
956 $providers = $this->getPreAuthenticationProviders() +
957 $this->getPrimaryAuthenticationProviders() +
958 $this->getSecondaryAuthenticationProviders();
959 foreach ( $providers as $provider ) {
960 $status = $provider->testUserForCreation( $user, false, $options );
961 if ( !$status->isGood() ) {
962 return Status
::wrap( $status );
966 return Status
::newGood();
970 * Basic permissions checks on whether a user can create accounts
971 * @param User $creator User doing the account creation
974 public function checkAccountCreatePermissions( User
$creator ) {
975 // Wiki is read-only?
976 if ( wfReadOnly() ) {
977 return Status
::newFatal( 'readonlytext', wfReadOnlyReason() );
980 // This is awful, this permission check really shouldn't go through Title.
981 $permErrors = \SpecialPage
::getTitleFor( 'CreateAccount' )
982 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
984 $status = Status
::newGood();
985 foreach ( $permErrors as $args ) {
986 call_user_func_array( [ $status, 'fatal' ], $args );
991 $block = $creator->isBlockedFromCreateAccount();
995 $block->mReason ?
: wfMessage( 'blockednoreason' )->text(),
999 if ( $block->getType() === \Block
::TYPE_RANGE
) {
1000 $errorMessage = 'cantcreateaccount-range-text';
1001 $errorParams[] = $this->getRequest()->getIP();
1003 $errorMessage = 'cantcreateaccount-text';
1006 return Status
::newFatal( wfMessage( $errorMessage, $errorParams ) );
1009 $ip = $this->getRequest()->getIP();
1010 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1011 return Status
::newFatal( 'sorbs_create_account_reason' );
1014 return Status
::newGood();
1018 * Start an account creation flow
1020 * In addition to the AuthenticationRequests returned by
1021 * $this->getAuthenticationRequests(), a client might include a
1022 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
1024 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
1026 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
1027 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
1028 * username set, that username must be used for all other requests.
1030 * @param User $creator User doing the account creation
1031 * @param AuthenticationRequest[] $reqs
1032 * @param string $returnToUrl Url that REDIRECT responses should eventually
1034 * @return AuthenticationResponse
1036 public function beginAccountCreation( User
$creator, array $reqs, $returnToUrl ) {
1037 $session = $this->request
->getSession();
1038 if ( !$this->canCreateAccounts() ) {
1039 // Caller should have called canCreateAccounts()
1040 $session->remove( 'AuthManager::accountCreationState' );
1041 throw new \
LogicException( 'Account creation is not possible' );
1045 $username = AuthenticationRequest
::getUsernameFromRequests( $reqs );
1046 } catch ( \UnexpectedValueException
$ex ) {
1049 if ( $username === null ) {
1050 $this->logger
->debug( __METHOD__
. ': No username provided' );
1051 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1054 // Permissions check
1055 $status = $this->checkAccountCreatePermissions( $creator );
1056 if ( !$status->isGood() ) {
1057 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1058 'user' => $username,
1059 'creator' => $creator->getName(),
1060 'reason' => $status->getWikiText( null, null, 'en' )
1062 return AuthenticationResponse
::newFail( $status->getMessage() );
1065 $status = $this->canCreateAccount(
1066 $username, [ 'flags' => User
::READ_LOCKING
, 'creating' => true ]
1068 if ( !$status->isGood() ) {
1069 $this->logger
->debug( __METHOD__
. ': {user} cannot be created: {reason}', [
1070 'user' => $username,
1071 'creator' => $creator->getName(),
1072 'reason' => $status->getWikiText( null, null, 'en' )
1074 return AuthenticationResponse
::newFail( $status->getMessage() );
1077 $user = User
::newFromName( $username, 'creatable' );
1078 foreach ( $reqs as $req ) {
1079 $req->username
= $username;
1080 $req->returnToUrl
= $returnToUrl;
1081 if ( $req instanceof UserDataAuthenticationRequest
) {
1082 $status = $req->populateUser( $user );
1083 if ( !$status->isGood() ) {
1084 $status = Status
::wrap( $status );
1085 $session->remove( 'AuthManager::accountCreationState' );
1086 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1087 'user' => $user->getName(),
1088 'creator' => $creator->getName(),
1089 'reason' => $status->getWikiText( null, null, 'en' ),
1091 return AuthenticationResponse
::newFail( $status->getMessage() );
1096 $this->removeAuthenticationSessionData( null );
1099 'username' => $username,
1101 'creatorid' => $creator->getId(),
1102 'creatorname' => $creator->getName(),
1104 'returnToUrl' => $returnToUrl,
1106 'primaryResponse' => null,
1108 'continueRequests' => [],
1110 'ranPreTests' => false,
1113 // Special case: converting a login to an account creation
1114 $req = AuthenticationRequest
::getRequestByClass(
1115 $reqs, CreateFromLoginAuthenticationRequest
::class
1118 $state['maybeLink'] = $req->maybeLink
;
1120 if ( $req->createRequest
) {
1121 $reqs[] = $req->createRequest
;
1122 $state['reqs'][] = $req->createRequest
;
1126 $session->setSecret( 'AuthManager::accountCreationState', $state );
1127 $session->persist();
1129 return $this->continueAccountCreation( $reqs );
1133 * Continue an account creation flow
1134 * @param AuthenticationRequest[] $reqs
1135 * @return AuthenticationResponse
1137 public function continueAccountCreation( array $reqs ) {
1138 $session = $this->request
->getSession();
1140 if ( !$this->canCreateAccounts() ) {
1141 // Caller should have called canCreateAccounts()
1142 $session->remove( 'AuthManager::accountCreationState' );
1143 throw new \
LogicException( 'Account creation is not possible' );
1146 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1147 if ( !is_array( $state ) ) {
1148 return AuthenticationResponse
::newFail(
1149 wfMessage( 'authmanager-create-not-in-progress' )
1152 $state['continueRequests'] = [];
1154 // Step 0: Prepare and validate the input
1156 $user = User
::newFromName( $state['username'], 'creatable' );
1157 if ( !is_object( $user ) ) {
1158 $session->remove( 'AuthManager::accountCreationState' );
1159 $this->logger
->debug( __METHOD__
. ': Invalid username', [
1160 'user' => $state['username'],
1162 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1165 if ( $state['creatorid'] ) {
1166 $creator = User
::newFromId( $state['creatorid'] );
1168 $creator = new User
;
1169 $creator->setName( $state['creatorname'] );
1172 // Avoid account creation races on double submissions
1173 $cache = \ObjectCache
::getLocalClusterInstance();
1174 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1176 // Don't clear AuthManager::accountCreationState for this code
1177 // path because the process that won the race owns it.
1178 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1179 'user' => $user->getName(),
1180 'creator' => $creator->getName(),
1182 return AuthenticationResponse
::newFail( wfMessage( 'usernameinprogress' ) );
1185 // Permissions check
1186 $status = $this->checkAccountCreatePermissions( $creator );
1187 if ( !$status->isGood() ) {
1188 $this->logger
->debug( __METHOD__
. ': {creator} cannot create users: {reason}', [
1189 'user' => $user->getName(),
1190 'creator' => $creator->getName(),
1191 'reason' => $status->getWikiText( null, null, 'en' )
1193 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1194 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1195 $session->remove( 'AuthManager::accountCreationState' );
1199 // Load from master for existence check
1200 $user->load( User
::READ_LOCKING
);
1202 if ( $state['userid'] === 0 ) {
1203 if ( $user->getId() != 0 ) {
1204 $this->logger
->debug( __METHOD__
. ': User exists locally', [
1205 'user' => $user->getName(),
1206 'creator' => $creator->getName(),
1208 $ret = AuthenticationResponse
::newFail( wfMessage( 'userexists' ) );
1209 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1210 $session->remove( 'AuthManager::accountCreationState' );
1214 if ( $user->getId() == 0 ) {
1215 $this->logger
->debug( __METHOD__
. ': User does not exist locally when it should', [
1216 'user' => $user->getName(),
1217 'creator' => $creator->getName(),
1218 'expected_id' => $state['userid'],
1220 throw new \
UnexpectedValueException(
1221 "User \"{$state['username']}\" should exist now, but doesn't!"
1224 if ( $user->getId() != $state['userid'] ) {
1225 $this->logger
->debug( __METHOD__
. ': User ID/name mismatch', [
1226 'user' => $user->getName(),
1227 'creator' => $creator->getName(),
1228 'expected_id' => $state['userid'],
1229 'actual_id' => $user->getId(),
1231 throw new \
UnexpectedValueException(
1232 "User \"{$state['username']}\" exists, but " .
1233 "ID {$user->getId()} != {$state['userid']}!"
1237 foreach ( $state['reqs'] as $req ) {
1238 if ( $req instanceof UserDataAuthenticationRequest
) {
1239 $status = $req->populateUser( $user );
1240 if ( !$status->isGood() ) {
1241 // This should never happen...
1242 $status = Status
::wrap( $status );
1243 $this->logger
->debug( __METHOD__
. ': UserData is invalid: {reason}', [
1244 'user' => $user->getName(),
1245 'creator' => $creator->getName(),
1246 'reason' => $status->getWikiText( null, null, 'en' ),
1248 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1249 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1250 $session->remove( 'AuthManager::accountCreationState' );
1256 foreach ( $reqs as $req ) {
1257 $req->returnToUrl
= $state['returnToUrl'];
1258 $req->username
= $state['username'];
1261 // Run pre-creation tests, if we haven't already
1262 if ( !$state['ranPreTests'] ) {
1263 $providers = $this->getPreAuthenticationProviders() +
1264 $this->getPrimaryAuthenticationProviders() +
1265 $this->getSecondaryAuthenticationProviders();
1266 foreach ( $providers as $id => $provider ) {
1267 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1268 if ( !$status->isGood() ) {
1269 $this->logger
->debug( __METHOD__
. ": Fail in pre-authentication by $id", [
1270 'user' => $user->getName(),
1271 'creator' => $creator->getName(),
1273 $ret = AuthenticationResponse
::newFail(
1274 Status
::wrap( $status )->getMessage()
1276 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1277 $session->remove( 'AuthManager::accountCreationState' );
1282 $state['ranPreTests'] = true;
1285 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1287 if ( $state['primary'] === null ) {
1288 // We haven't picked a PrimaryAuthenticationProvider yet
1289 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1290 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_NONE
) {
1293 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1294 switch ( $res->status
) {
1295 case AuthenticationResponse
::PASS
;
1296 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1297 'user' => $user->getName(),
1298 'creator' => $creator->getName(),
1300 $state['primary'] = $id;
1301 $state['primaryResponse'] = $res;
1303 case AuthenticationResponse
::FAIL
;
1304 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1305 'user' => $user->getName(),
1306 'creator' => $creator->getName(),
1308 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1309 $session->remove( 'AuthManager::accountCreationState' );
1311 case AuthenticationResponse
::ABSTAIN
;
1314 case AuthenticationResponse
::REDIRECT
;
1315 case AuthenticationResponse
::UI
;
1316 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1317 'user' => $user->getName(),
1318 'creator' => $creator->getName(),
1320 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1321 $state['primary'] = $id;
1322 $state['continueRequests'] = $res->neededRequests
;
1323 $session->setSecret( 'AuthManager::accountCreationState', $state );
1326 // @codeCoverageIgnoreStart
1328 throw new \
DomainException(
1329 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1331 // @codeCoverageIgnoreEnd
1334 if ( $state['primary'] === null ) {
1335 $this->logger
->debug( __METHOD__
. ': Primary creation failed because no provider accepted', [
1336 'user' => $user->getName(),
1337 'creator' => $creator->getName(),
1339 $ret = AuthenticationResponse
::newFail(
1340 wfMessage( 'authmanager-create-no-primary' )
1342 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1343 $session->remove( 'AuthManager::accountCreationState' );
1346 } elseif ( $state['primaryResponse'] === null ) {
1347 $provider = $this->getAuthenticationProvider( $state['primary'] );
1348 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1349 // Configuration changed? Force them to start over.
1350 // @codeCoverageIgnoreStart
1351 $ret = AuthenticationResponse
::newFail(
1352 wfMessage( 'authmanager-create-not-in-progress' )
1354 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1355 $session->remove( 'AuthManager::accountCreationState' );
1357 // @codeCoverageIgnoreEnd
1359 $id = $provider->getUniqueId();
1360 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1361 switch ( $res->status
) {
1362 case AuthenticationResponse
::PASS
;
1363 $this->logger
->debug( __METHOD__
. ": Primary creation passed by $id", [
1364 'user' => $user->getName(),
1365 'creator' => $creator->getName(),
1367 $state['primaryResponse'] = $res;
1369 case AuthenticationResponse
::FAIL
;
1370 $this->logger
->debug( __METHOD__
. ": Primary creation failed by $id", [
1371 'user' => $user->getName(),
1372 'creator' => $creator->getName(),
1374 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1375 $session->remove( 'AuthManager::accountCreationState' );
1377 case AuthenticationResponse
::REDIRECT
;
1378 case AuthenticationResponse
::UI
;
1379 $this->logger
->debug( __METHOD__
. ": Primary creation $res->status by $id", [
1380 'user' => $user->getName(),
1381 'creator' => $creator->getName(),
1383 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1384 $state['continueRequests'] = $res->neededRequests
;
1385 $session->setSecret( 'AuthManager::accountCreationState', $state );
1388 throw new \
DomainException(
1389 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1394 // Step 2: Primary authentication succeeded, create the User object
1395 // and add the user locally.
1397 if ( $state['userid'] === 0 ) {
1398 $this->logger
->info( 'Creating user {user} during account creation', [
1399 'user' => $user->getName(),
1400 'creator' => $creator->getName(),
1402 $status = $user->addToDatabase();
1403 if ( !$status->isOK() ) {
1404 // @codeCoverageIgnoreStart
1405 $ret = AuthenticationResponse
::newFail( $status->getMessage() );
1406 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1407 $session->remove( 'AuthManager::accountCreationState' );
1409 // @codeCoverageIgnoreEnd
1411 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1412 \Hooks
::run( 'LocalUserCreated', [ $user, false ] );
1413 $user->saveSettings();
1414 $state['userid'] = $user->getId();
1416 // Update user count
1417 \DeferredUpdates
::addUpdate( new \
SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1419 // Watch user's userpage and talk page
1420 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1422 // Inform the provider
1423 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1426 if ( $this->config
->get( 'NewUserLog' ) ) {
1427 $isAnon = $creator->isAnon();
1428 $logEntry = new \
ManualLogEntry(
1430 $logSubtype ?
: ( $isAnon ?
'create' : 'create2' )
1432 $logEntry->setPerformer( $isAnon ?
$user : $creator );
1433 $logEntry->setTarget( $user->getUserPage() );
1434 /** @var CreationReasonAuthenticationRequest $req */
1435 $req = AuthenticationRequest
::getRequestByClass(
1436 $state['reqs'], CreationReasonAuthenticationRequest
::class
1438 $logEntry->setComment( $req ?
$req->reason
: '' );
1439 $logEntry->setParameters( [
1440 '4::userid' => $user->getId(),
1442 $logid = $logEntry->insert();
1443 $logEntry->publish( $logid );
1447 // Step 3: Iterate over all the secondary authentication providers.
1449 $beginReqs = $state['reqs'];
1451 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1452 if ( !isset( $state['secondary'][$id] ) ) {
1453 // This provider isn't started yet, so we pass it the set
1454 // of reqs from beginAuthentication instead of whatever
1455 // might have been used by a previous provider in line.
1456 $func = 'beginSecondaryAccountCreation';
1457 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1458 } elseif ( !$state['secondary'][$id] ) {
1459 $func = 'continueSecondaryAccountCreation';
1460 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1464 switch ( $res->status
) {
1465 case AuthenticationResponse
::PASS
;
1466 $this->logger
->debug( __METHOD__
. ": Secondary creation passed by $id", [
1467 'user' => $user->getName(),
1468 'creator' => $creator->getName(),
1471 case AuthenticationResponse
::ABSTAIN
;
1472 $state['secondary'][$id] = true;
1474 case AuthenticationResponse
::REDIRECT
;
1475 case AuthenticationResponse
::UI
;
1476 $this->logger
->debug( __METHOD__
. ": Secondary creation $res->status by $id", [
1477 'user' => $user->getName(),
1478 'creator' => $creator->getName(),
1480 $this->fillRequests( $res->neededRequests
, self
::ACTION_CREATE
, null );
1481 $state['secondary'][$id] = false;
1482 $state['continueRequests'] = $res->neededRequests
;
1483 $session->setSecret( 'AuthManager::accountCreationState', $state );
1485 case AuthenticationResponse
::FAIL
;
1486 throw new \
DomainException(
1487 get_class( $provider ) . "::{$func}() returned $res->status." .
1488 ' Secondary providers are not allowed to fail account creation, that' .
1489 ' should have been done via testForAccountCreation().'
1491 // @codeCoverageIgnoreStart
1493 throw new \
DomainException(
1494 get_class( $provider ) . "::{$func}() returned $res->status"
1496 // @codeCoverageIgnoreEnd
1500 $id = $user->getId();
1501 $name = $user->getName();
1502 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1503 $ret = AuthenticationResponse
::newPass( $name );
1504 $ret->loginRequest
= $req;
1505 $this->createdAccountAuthenticationRequests
[] = $req;
1507 $this->logger
->info( __METHOD__
. ': Account creation succeeded for {user}', [
1508 'user' => $user->getName(),
1509 'creator' => $creator->getName(),
1512 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1513 $session->remove( 'AuthManager::accountCreationState' );
1514 $this->removeAuthenticationSessionData( null );
1516 } catch ( \Exception
$ex ) {
1517 $session->remove( 'AuthManager::accountCreationState' );
1523 * Auto-create an account, and log into that account
1525 * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
1526 * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
1527 * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
1528 * the username of a non-existing user from provideSessionInfo(). Calling this method
1529 * explicitly (e.g. from a maintenance script) is also fine.
1531 * @param User $user User to auto-create
1532 * @param string $source What caused the auto-creation? This must be the ID
1533 * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1534 * @param bool $login Whether to also log the user in
1535 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1537 public function autoCreateUser( User
$user, $source, $login = true ) {
1538 if ( $source !== self
::AUTOCREATE_SOURCE_SESSION
&&
1539 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1541 throw new \
InvalidArgumentException( "Unknown auto-creation source: $source" );
1544 $username = $user->getName();
1546 // Try the local user from the replica DB
1547 $localId = User
::idFromName( $username );
1548 $flags = User
::READ_NORMAL
;
1550 // Fetch the user ID from the master, so that we don't try to create the user
1551 // when they already exist, due to replication lag
1552 // @codeCoverageIgnoreStart
1553 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1554 $localId = User
::idFromName( $username, User
::READ_LATEST
);
1555 $flags = User
::READ_LATEST
;
1557 // @codeCoverageIgnoreEnd
1560 $this->logger
->debug( __METHOD__
. ': {username} already exists locally', [
1561 'username' => $username,
1563 $user->setId( $localId );
1564 $user->loadFromId( $flags );
1566 $this->setSessionDataForUser( $user );
1568 $status = Status
::newGood();
1569 $status->warning( 'userexists' );
1573 // Wiki is read-only?
1574 if ( wfReadOnly() ) {
1575 $this->logger
->debug( __METHOD__
. ': denied by wfReadOnly(): {reason}', [
1576 'username' => $username,
1577 'reason' => wfReadOnlyReason(),
1580 $user->loadFromId();
1581 return Status
::newFatal( 'readonlytext', wfReadOnlyReason() );
1584 // Check the session, if we tried to create this user already there's
1585 // no point in retrying.
1586 $session = $this->request
->getSession();
1587 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1588 $this->logger
->debug( __METHOD__
. ': blacklisted in session {sessionid}', [
1589 'username' => $username,
1590 'sessionid' => $session->getId(),
1593 $user->loadFromId();
1594 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1595 if ( $reason instanceof StatusValue
) {
1596 return Status
::wrap( $reason );
1598 return Status
::newFatal( $reason );
1602 // Is the username creatable?
1603 if ( !User
::isCreatableName( $username ) ) {
1604 $this->logger
->debug( __METHOD__
. ': name "{username}" is not creatable', [
1605 'username' => $username,
1607 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1609 $user->loadFromId();
1610 return Status
::newFatal( 'noname' );
1613 // Is the IP user able to create accounts?
1615 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1616 $this->logger
->debug( __METHOD__
. ': IP lacks the ability to create or autocreate accounts', [
1617 'username' => $username,
1618 'ip' => $anon->getName(),
1620 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1621 $session->persist();
1623 $user->loadFromId();
1624 return Status
::newFatal( 'authmanager-autocreate-noperm' );
1627 // Avoid account creation races on double submissions
1628 $cache = \ObjectCache
::getLocalClusterInstance();
1629 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1631 $this->logger
->debug( __METHOD__
. ': Could not acquire account creation lock', [
1632 'user' => $username,
1635 $user->loadFromId();
1636 return Status
::newFatal( 'usernameinprogress' );
1639 // Denied by providers?
1641 'flags' => User
::READ_LATEST
,
1644 $providers = $this->getPreAuthenticationProviders() +
1645 $this->getPrimaryAuthenticationProviders() +
1646 $this->getSecondaryAuthenticationProviders();
1647 foreach ( $providers as $provider ) {
1648 $status = $provider->testUserForCreation( $user, $source, $options );
1649 if ( !$status->isGood() ) {
1650 $ret = Status
::wrap( $status );
1651 $this->logger
->debug( __METHOD__
. ': Provider denied creation of {username}: {reason}', [
1652 'username' => $username,
1653 'reason' => $ret->getWikiText( null, null, 'en' ),
1655 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1657 $user->loadFromId();
1662 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1663 if ( $cache->get( $backoffKey ) ) {
1664 $this->logger
->debug( __METHOD__
. ': {username} denied by prior creation attempt failures', [
1665 'username' => $username,
1668 $user->loadFromId();
1669 return Status
::newFatal( 'authmanager-autocreate-exception' );
1672 // Checks passed, create the user...
1673 $from = isset( $_SERVER['REQUEST_URI'] ) ?
$_SERVER['REQUEST_URI'] : 'CLI';
1674 $this->logger
->info( __METHOD__
. ': creating new user ({username}) - from: {from}', [
1675 'username' => $username,
1679 // Ignore warnings about master connections/writes...hard to avoid here
1680 $trxProfiler = \Profiler
::instance()->getTransactionProfiler();
1681 $old = $trxProfiler->setSilenced( true );
1683 $status = $user->addToDatabase();
1684 if ( !$status->isOK() ) {
1685 // Double-check for a race condition (T70012). We make use of the fact that when
1686 // addToDatabase fails due to the user already existing, the user object gets loaded.
1687 if ( $user->getId() ) {
1688 $this->logger
->info( __METHOD__
. ': {username} already exists locally (race)', [
1689 'username' => $username,
1692 $this->setSessionDataForUser( $user );
1694 $status = Status
::newGood();
1695 $status->warning( 'userexists' );
1697 $this->logger
->error( __METHOD__
. ': {username} failed with message {msg}', [
1698 'username' => $username,
1699 'msg' => $status->getWikiText( null, null, 'en' )
1702 $user->loadFromId();
1706 } catch ( \Exception
$ex ) {
1707 $trxProfiler->setSilenced( $old );
1708 $this->logger
->error( __METHOD__
. ': {username} failed with exception {exception}', [
1709 'username' => $username,
1712 // Do not keep throwing errors for a while
1713 $cache->set( $backoffKey, 1, 600 );
1714 // Bubble up error; which should normally trigger DB rollbacks
1718 $this->setDefaultUserOptions( $user, false );
1720 // Inform the providers
1721 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1723 \Hooks
::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1724 \Hooks
::run( 'LocalUserCreated', [ $user, true ] );
1725 $user->saveSettings();
1727 // Update user count
1728 \DeferredUpdates
::addUpdate( new \
SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1729 // Watch user's userpage and talk page
1730 \DeferredUpdates
::addCallableUpdate( function () use ( $user ) {
1731 $user->addWatch( $user->getUserPage(), User
::IGNORE_USER_RIGHTS
);
1735 if ( $this->config
->get( 'NewUserLog' ) ) {
1736 $logEntry = new \
ManualLogEntry( 'newusers', 'autocreate' );
1737 $logEntry->setPerformer( $user );
1738 $logEntry->setTarget( $user->getUserPage() );
1739 $logEntry->setComment( '' );
1740 $logEntry->setParameters( [
1741 '4::userid' => $user->getId(),
1743 $logEntry->insert();
1746 $trxProfiler->setSilenced( $old );
1749 $this->setSessionDataForUser( $user );
1752 return Status
::newGood();
1758 * @name Account linking
1763 * Determine whether accounts can be linked
1766 public function canLinkAccounts() {
1767 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1768 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
) {
1776 * Start an account linking flow
1778 * @param User $user User being linked
1779 * @param AuthenticationRequest[] $reqs
1780 * @param string $returnToUrl Url that REDIRECT responses should eventually
1782 * @return AuthenticationResponse
1784 public function beginAccountLink( User
$user, array $reqs, $returnToUrl ) {
1785 $session = $this->request
->getSession();
1786 $session->remove( 'AuthManager::accountLinkState' );
1788 if ( !$this->canLinkAccounts() ) {
1789 // Caller should have called canLinkAccounts()
1790 throw new \
LogicException( 'Account linking is not possible' );
1793 if ( $user->getId() === 0 ) {
1794 if ( !User
::isUsableName( $user->getName() ) ) {
1795 $msg = wfMessage( 'noname' );
1797 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1799 return AuthenticationResponse
::newFail( $msg );
1801 foreach ( $reqs as $req ) {
1802 $req->username
= $user->getName();
1803 $req->returnToUrl
= $returnToUrl;
1806 $this->removeAuthenticationSessionData( null );
1808 $providers = $this->getPreAuthenticationProviders();
1809 foreach ( $providers as $id => $provider ) {
1810 $status = $provider->testForAccountLink( $user );
1811 if ( !$status->isGood() ) {
1812 $this->logger
->debug( __METHOD__
. ": Account linking pre-check failed by $id", [
1813 'user' => $user->getName(),
1815 $ret = AuthenticationResponse
::newFail(
1816 Status
::wrap( $status )->getMessage()
1818 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1824 'username' => $user->getName(),
1825 'userid' => $user->getId(),
1826 'returnToUrl' => $returnToUrl,
1828 'continueRequests' => [],
1831 $providers = $this->getPrimaryAuthenticationProviders();
1832 foreach ( $providers as $id => $provider ) {
1833 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider
::TYPE_LINK
) {
1837 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1838 switch ( $res->status
) {
1839 case AuthenticationResponse
::PASS
;
1840 $this->logger
->info( "Account linked to {user} by $id", [
1841 'user' => $user->getName(),
1843 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1846 case AuthenticationResponse
::FAIL
;
1847 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1848 'user' => $user->getName(),
1850 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1853 case AuthenticationResponse
::ABSTAIN
;
1857 case AuthenticationResponse
::REDIRECT
;
1858 case AuthenticationResponse
::UI
;
1859 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1860 'user' => $user->getName(),
1862 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1863 $state['primary'] = $id;
1864 $state['continueRequests'] = $res->neededRequests
;
1865 $session->setSecret( 'AuthManager::accountLinkState', $state );
1866 $session->persist();
1869 // @codeCoverageIgnoreStart
1871 throw new \
DomainException(
1872 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1874 // @codeCoverageIgnoreEnd
1878 $this->logger
->debug( __METHOD__
. ': Account linking failed because no provider accepted', [
1879 'user' => $user->getName(),
1881 $ret = AuthenticationResponse
::newFail(
1882 wfMessage( 'authmanager-link-no-primary' )
1884 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1889 * Continue an account linking flow
1890 * @param AuthenticationRequest[] $reqs
1891 * @return AuthenticationResponse
1893 public function continueAccountLink( array $reqs ) {
1894 $session = $this->request
->getSession();
1896 if ( !$this->canLinkAccounts() ) {
1897 // Caller should have called canLinkAccounts()
1898 $session->remove( 'AuthManager::accountLinkState' );
1899 throw new \
LogicException( 'Account linking is not possible' );
1902 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1903 if ( !is_array( $state ) ) {
1904 return AuthenticationResponse
::newFail(
1905 wfMessage( 'authmanager-link-not-in-progress' )
1908 $state['continueRequests'] = [];
1910 // Step 0: Prepare and validate the input
1912 $user = User
::newFromName( $state['username'], 'usable' );
1913 if ( !is_object( $user ) ) {
1914 $session->remove( 'AuthManager::accountLinkState' );
1915 return AuthenticationResponse
::newFail( wfMessage( 'noname' ) );
1917 if ( $user->getId() != $state['userid'] ) {
1918 throw new \
UnexpectedValueException(
1919 "User \"{$state['username']}\" is valid, but " .
1920 "ID {$user->getId()} != {$state['userid']}!"
1924 foreach ( $reqs as $req ) {
1925 $req->username
= $state['username'];
1926 $req->returnToUrl
= $state['returnToUrl'];
1929 // Step 1: Call the primary again until it succeeds
1931 $provider = $this->getAuthenticationProvider( $state['primary'] );
1932 if ( !$provider instanceof PrimaryAuthenticationProvider
) {
1933 // Configuration changed? Force them to start over.
1934 // @codeCoverageIgnoreStart
1935 $ret = AuthenticationResponse
::newFail(
1936 wfMessage( 'authmanager-link-not-in-progress' )
1938 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1939 $session->remove( 'AuthManager::accountLinkState' );
1941 // @codeCoverageIgnoreEnd
1943 $id = $provider->getUniqueId();
1944 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1945 switch ( $res->status
) {
1946 case AuthenticationResponse
::PASS
;
1947 $this->logger
->info( "Account linked to {user} by $id", [
1948 'user' => $user->getName(),
1950 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1951 $session->remove( 'AuthManager::accountLinkState' );
1953 case AuthenticationResponse
::FAIL
;
1954 $this->logger
->debug( __METHOD__
. ": Account linking failed by $id", [
1955 'user' => $user->getName(),
1957 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1958 $session->remove( 'AuthManager::accountLinkState' );
1960 case AuthenticationResponse
::REDIRECT
;
1961 case AuthenticationResponse
::UI
;
1962 $this->logger
->debug( __METHOD__
. ": Account linking $res->status by $id", [
1963 'user' => $user->getName(),
1965 $this->fillRequests( $res->neededRequests
, self
::ACTION_LINK
, $user->getName() );
1966 $state['continueRequests'] = $res->neededRequests
;
1967 $session->setSecret( 'AuthManager::accountLinkState', $state );
1970 throw new \
DomainException(
1971 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1974 } catch ( \Exception
$ex ) {
1975 $session->remove( 'AuthManager::accountLinkState' );
1983 * @name Information methods
1988 * Return the applicable list of AuthenticationRequests
1990 * Possible values for $action:
1991 * - ACTION_LOGIN: Valid for passing to beginAuthentication
1992 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1993 * - ACTION_CREATE: Valid for passing to beginAccountCreation
1994 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1995 * - ACTION_LINK: Valid for passing to beginAccountLink
1996 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1997 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1998 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1999 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
2001 * @param string $action One of the AuthManager::ACTION_* constants
2002 * @param User|null $user User being acted on, instead of the current user.
2003 * @return AuthenticationRequest[]
2005 public function getAuthenticationRequests( $action, User
$user = null ) {
2007 $providerAction = $action;
2009 // Figure out which providers to query
2010 switch ( $action ) {
2011 case self
::ACTION_LOGIN
:
2012 case self
::ACTION_CREATE
:
2013 $providers = $this->getPreAuthenticationProviders() +
2014 $this->getPrimaryAuthenticationProviders() +
2015 $this->getSecondaryAuthenticationProviders();
2018 case self
::ACTION_LOGIN_CONTINUE
:
2019 $state = $this->request
->getSession()->getSecret( 'AuthManager::authnState' );
2020 return is_array( $state ) ?
$state['continueRequests'] : [];
2022 case self
::ACTION_CREATE_CONTINUE
:
2023 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountCreationState' );
2024 return is_array( $state ) ?
$state['continueRequests'] : [];
2026 case self
::ACTION_LINK
:
2027 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2028 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2032 case self
::ACTION_UNLINK
:
2033 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
2034 return $p->accountCreationType() === PrimaryAuthenticationProvider
::TYPE_LINK
;
2037 // To providers, unlink and remove are identical.
2038 $providerAction = self
::ACTION_REMOVE
;
2041 case self
::ACTION_LINK_CONTINUE
:
2042 $state = $this->request
->getSession()->getSecret( 'AuthManager::accountLinkState' );
2043 return is_array( $state ) ?
$state['continueRequests'] : [];
2045 case self
::ACTION_CHANGE
:
2046 case self
::ACTION_REMOVE
:
2047 $providers = $this->getPrimaryAuthenticationProviders() +
2048 $this->getSecondaryAuthenticationProviders();
2051 // @codeCoverageIgnoreStart
2053 throw new \
DomainException( __METHOD__
. ": Invalid action \"$action\"" );
2055 // @codeCoverageIgnoreEnd
2057 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2061 * Internal request lookup for self::getAuthenticationRequests
2063 * @param string $providerAction Action to pass to providers
2064 * @param array $options Options to pass to providers
2065 * @param AuthenticationProvider[] $providers
2066 * @param User|null $user
2067 * @return AuthenticationRequest[]
2069 private function getAuthenticationRequestsInternal(
2070 $providerAction, array $options, array $providers, User
$user = null
2072 $user = $user ?
: \RequestContext
::getMain()->getUser();
2073 $options['username'] = $user->isAnon() ?
null : $user->getName();
2075 // Query them and merge results
2077 foreach ( $providers as $provider ) {
2078 $isPrimary = $provider instanceof PrimaryAuthenticationProvider
;
2079 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2080 $id = $req->getUniqueId();
2082 // If a required request if from a Primary, mark it as "primary-required" instead
2084 if ( $req->required
) {
2085 $req->required
= AuthenticationRequest
::PRIMARY_REQUIRED
;
2090 !isset( $reqs[$id] )
2091 ||
$req->required
=== AuthenticationRequest
::REQUIRED
2092 ||
$reqs[$id] === AuthenticationRequest
::OPTIONAL
2099 // AuthManager has its own req for some actions
2100 switch ( $providerAction ) {
2101 case self
::ACTION_LOGIN
:
2102 $reqs[] = new RememberMeAuthenticationRequest
;
2105 case self
::ACTION_CREATE
:
2106 $reqs[] = new UsernameAuthenticationRequest
;
2107 $reqs[] = new UserDataAuthenticationRequest
;
2108 if ( $options['username'] !== null ) {
2109 $reqs[] = new CreationReasonAuthenticationRequest
;
2110 $options['username'] = null; // Don't fill in the username below
2115 // Fill in reqs data
2116 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2118 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2119 if ( $providerAction === self
::ACTION_CHANGE ||
$providerAction === self
::ACTION_REMOVE
) {
2120 $reqs = array_filter( $reqs, function ( $req ) {
2121 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2125 return array_values( $reqs );
2129 * Set values in an array of requests
2130 * @param AuthenticationRequest[] &$reqs
2131 * @param string $action
2132 * @param string|null $username
2133 * @param boolean $forceAction
2135 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2136 foreach ( $reqs as $req ) {
2137 if ( !$req->action ||
$forceAction ) {
2138 $req->action
= $action;
2140 if ( $req->username
=== null ) {
2141 $req->username
= $username;
2147 * Determine whether a username exists
2148 * @param string $username
2149 * @param int $flags Bitfield of User:READ_* constants
2152 public function userExists( $username, $flags = User
::READ_NORMAL
) {
2153 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2154 if ( $provider->testUserExists( $username, $flags ) ) {
2163 * Determine whether a user property should be allowed to be changed.
2165 * Supported properties are:
2170 * @param string $property
2173 public function allowsPropertyChange( $property ) {
2174 $providers = $this->getPrimaryAuthenticationProviders() +
2175 $this->getSecondaryAuthenticationProviders();
2176 foreach ( $providers as $provider ) {
2177 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2185 * Get a provider by ID
2186 * @note This is public so extensions can check whether their own provider
2187 * is installed and so they can read its configuration if necessary.
2188 * Other uses are not recommended.
2190 * @return AuthenticationProvider|null
2192 public function getAuthenticationProvider( $id ) {
2194 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2195 return $this->allAuthenticationProviders
[$id];
2198 // Slow version: instantiate each kind and check
2199 $providers = $this->getPrimaryAuthenticationProviders();
2200 if ( isset( $providers[$id] ) ) {
2201 return $providers[$id];
2203 $providers = $this->getSecondaryAuthenticationProviders();
2204 if ( isset( $providers[$id] ) ) {
2205 return $providers[$id];
2207 $providers = $this->getPreAuthenticationProviders();
2208 if ( isset( $providers[$id] ) ) {
2209 return $providers[$id];
2218 * @name Internal methods
2223 * Store authentication in the current session
2224 * @protected For use by AuthenticationProviders
2225 * @param string $key
2226 * @param mixed $data Must be serializable
2228 public function setAuthenticationSessionData( $key, $data ) {
2229 $session = $this->request
->getSession();
2230 $arr = $session->getSecret( 'authData' );
2231 if ( !is_array( $arr ) ) {
2235 $session->setSecret( 'authData', $arr );
2239 * Fetch authentication data from the current session
2240 * @protected For use by AuthenticationProviders
2241 * @param string $key
2242 * @param mixed $default
2245 public function getAuthenticationSessionData( $key, $default = null ) {
2246 $arr = $this->request
->getSession()->getSecret( 'authData' );
2247 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2255 * Remove authentication data
2256 * @protected For use by AuthenticationProviders
2257 * @param string|null $key If null, all data is removed
2259 public function removeAuthenticationSessionData( $key ) {
2260 $session = $this->request
->getSession();
2261 if ( $key === null ) {
2262 $session->remove( 'authData' );
2264 $arr = $session->getSecret( 'authData' );
2265 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2266 unset( $arr[$key] );
2267 $session->setSecret( 'authData', $arr );
2273 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2274 * @param string $class
2275 * @param array[] $specs
2276 * @return AuthenticationProvider[]
2278 protected function providerArrayFromSpecs( $class, array $specs ) {
2280 foreach ( $specs as &$spec ) {
2281 $spec = [ 'sort2' => $i++
] +
$spec +
[ 'sort' => 0 ];
2284 usort( $specs, function ( $a, $b ) {
2285 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2286 ?
: $a['sort2'] - $b['sort2'];
2290 foreach ( $specs as $spec ) {
2291 $provider = \ObjectFactory
::getObjectFromSpec( $spec );
2292 if ( !$provider instanceof $class ) {
2293 throw new \
RuntimeException(
2294 "Expected instance of $class, got " . get_class( $provider )
2297 $provider->setLogger( $this->logger
);
2298 $provider->setManager( $this );
2299 $provider->setConfig( $this->config
);
2300 $id = $provider->getUniqueId();
2301 if ( isset( $this->allAuthenticationProviders
[$id] ) ) {
2302 throw new \
RuntimeException(
2303 "Duplicate specifications for id $id (classes " .
2304 get_class( $provider ) . ' and ' .
2305 get_class( $this->allAuthenticationProviders
[$id] ) . ')'
2308 $this->allAuthenticationProviders
[$id] = $provider;
2309 $ret[$id] = $provider;
2315 * Get the configuration
2318 private function getConfiguration() {
2319 return $this->config
->get( 'AuthManagerConfig' ) ?
: $this->config
->get( 'AuthManagerAutoConfig' );
2323 * Get the list of PreAuthenticationProviders
2324 * @return PreAuthenticationProvider[]
2326 protected function getPreAuthenticationProviders() {
2327 if ( $this->preAuthenticationProviders
=== null ) {
2328 $conf = $this->getConfiguration();
2329 $this->preAuthenticationProviders
= $this->providerArrayFromSpecs(
2330 PreAuthenticationProvider
::class, $conf['preauth']
2333 return $this->preAuthenticationProviders
;
2337 * Get the list of PrimaryAuthenticationProviders
2338 * @return PrimaryAuthenticationProvider[]
2340 protected function getPrimaryAuthenticationProviders() {
2341 if ( $this->primaryAuthenticationProviders
=== null ) {
2342 $conf = $this->getConfiguration();
2343 $this->primaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2344 PrimaryAuthenticationProvider
::class, $conf['primaryauth']
2347 return $this->primaryAuthenticationProviders
;
2351 * Get the list of SecondaryAuthenticationProviders
2352 * @return SecondaryAuthenticationProvider[]
2354 protected function getSecondaryAuthenticationProviders() {
2355 if ( $this->secondaryAuthenticationProviders
=== null ) {
2356 $conf = $this->getConfiguration();
2357 $this->secondaryAuthenticationProviders
= $this->providerArrayFromSpecs(
2358 SecondaryAuthenticationProvider
::class, $conf['secondaryauth']
2361 return $this->secondaryAuthenticationProviders
;
2367 * @param bool|null $remember
2369 private function setSessionDataForUser( $user, $remember = null ) {
2370 $session = $this->request
->getSession();
2371 $delay = $session->delaySave();
2373 $session->resetId();
2374 $session->resetAllTokens();
2375 if ( $session->canSetUser() ) {
2376 $session->setUser( $user );
2378 if ( $remember !== null ) {
2379 $session->setRememberUser( $remember );
2381 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2382 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2383 $session->persist();
2385 \Wikimedia\ScopedCallback
::consume( $delay );
2387 \Hooks
::run( 'UserLoggedIn', [ $user ] );
2392 * @param bool $useContextLang Use 'uselang' to set the user's language
2394 private function setDefaultUserOptions( User
$user, $useContextLang ) {
2399 $lang = $useContextLang ? \RequestContext
::getMain()->getLanguage() : $wgContLang;
2400 $user->setOption( 'language', $lang->getPreferredVariant() );
2402 if ( $wgContLang->hasVariants() ) {
2403 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2408 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2409 * @param string $method
2410 * @param array $args
2412 private function callMethodOnProviders( $which, $method, array $args ) {
2415 $providers +
= $this->getPreAuthenticationProviders();
2418 $providers +
= $this->getPrimaryAuthenticationProviders();
2421 $providers +
= $this->getSecondaryAuthenticationProviders();
2423 foreach ( $providers as $provider ) {
2424 call_user_func_array( [ $provider, $method ], $args );
2429 * Reset the internal caching for unit testing
2430 * @protected Unit tests only
2432 public static function resetCache() {
2433 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2434 // @codeCoverageIgnoreStart
2435 throw new \
MWException( __METHOD__
. ' may only be called from unit tests!' );
2436 // @codeCoverageIgnoreEnd
2439 self
::$instance = null;
2447 * For really cool vim folding this needs to be at the end:
2448 * vim: foldmarker=@{,@} foldmethod=marker