MediaWiki REL1_37
AuthManager.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Auth;
25
26use Config;
27use Language;
41use Psr\Log\LoggerAwareInterface;
42use Psr\Log\LoggerInterface;
43use Psr\Log\NullLogger;
44use ReadOnlyMode;
45use SpecialPage;
46use Status;
47use StatusValue;
48use User;
49use WebRequest;
50use Wikimedia\ObjectFactory;
52use Wikimedia\ScopedCallback;
53
102class AuthManager implements LoggerAwareInterface {
104 public const ACTION_LOGIN = 'login';
108 public const ACTION_LOGIN_CONTINUE = 'login-continue';
110 public const ACTION_CREATE = 'create';
114 public const ACTION_CREATE_CONTINUE = 'create-continue';
116 public const ACTION_LINK = 'link';
120 public const ACTION_LINK_CONTINUE = 'link-continue';
122 public const ACTION_CHANGE = 'change';
124 public const ACTION_REMOVE = 'remove';
126 public const ACTION_UNLINK = 'unlink';
127
129 public const SEC_OK = 'ok';
131 public const SEC_REAUTH = 'reauth';
133 public const SEC_FAIL = 'fail';
134
136 public const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
137
139 public const AUTOCREATE_SOURCE_MAINT = '::Maintenance::';
140
142 private $request;
143
145 private $config;
146
149
151 private $logger;
152
155
158
161
164
167
170
173
175 private $hookRunner;
176
179
182
185
188
191
194
197
200
203
206
224 public function __construct(
227 ObjectFactory $objectFactory,
240 ) {
241 $this->request = $request;
242 $this->config = $config;
243 $this->objectFactory = $objectFactory;
244 $this->hookContainer = $hookContainer;
245 $this->hookRunner = new HookRunner( $hookContainer );
246 $this->setLogger( new NullLogger() );
247 $this->readOnlyMode = $readOnlyMode;
248 $this->userNameUtils = $userNameUtils;
249 $this->blockManager = $blockManager;
250 $this->watchlistManager = $watchlistManager;
251 $this->loadBalancer = $loadBalancer;
252 $this->contentLanguage = $contentLanguage;
253 $this->languageConverterFactory = $languageConverterFactory;
254 $this->botPasswordStore = $botPasswordStore;
255 $this->userFactory = $userFactory;
256 $this->userIdentityLookup = $userIdentityLookup;
257 $this->userOptionsManager = $userOptionsManager;
258 }
259
263 public function setLogger( LoggerInterface $logger ) {
264 $this->logger = $logger;
265 }
266
270 public function getRequest() {
271 return $this->request;
272 }
273
280 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
281 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
282
283 if ( $this->primaryAuthenticationProviders !== null ) {
284 $this->logger->warning(
285 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
286 );
287
288 $this->allAuthenticationProviders = array_diff_key(
289 $this->allAuthenticationProviders,
290 $this->primaryAuthenticationProviders
291 );
292 $session = $this->request->getSession();
293 $session->remove( 'AuthManager::authnState' );
294 $session->remove( 'AuthManager::accountCreationState' );
295 $session->remove( 'AuthManager::accountLinkState' );
296 $this->createdAccountAuthenticationRequests = [];
297 }
298
299 $this->primaryAuthenticationProviders = [];
300 foreach ( $providers as $provider ) {
301 if ( !$provider instanceof AbstractPrimaryAuthenticationProvider ) {
302 throw new \RuntimeException(
303 'Expected instance of MediaWiki\\Auth\\AbstractPrimaryAuthenticationProvider, got ' .
304 get_class( $provider )
305 );
306 }
307 $provider->init( $this->logger, $this, $this->hookContainer, $this->config, $this->userNameUtils );
308 $id = $provider->getUniqueId();
309 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
310 throw new \RuntimeException(
311 "Duplicate specifications for id $id (classes " .
312 get_class( $provider ) . ' and ' .
313 get_class( $this->allAuthenticationProviders[$id] ) . ')'
314 );
315 }
316 $this->allAuthenticationProviders[$id] = $provider;
317 $this->primaryAuthenticationProviders[$id] = $provider;
318 }
319 }
320
321 /***************************************************************************/
322 // region Authentication
333 public function canAuthenticateNow() {
334 return $this->request->getSession()->canSetUser();
335 }
336
355 public function beginAuthentication( array $reqs, $returnToUrl ) {
356 $session = $this->request->getSession();
357 if ( !$session->canSetUser() ) {
358 // Caller should have called canAuthenticateNow()
359 $session->remove( 'AuthManager::authnState' );
360 throw new \LogicException( 'Authentication is not possible now' );
361 }
362
363 $guessUserName = null;
364 foreach ( $reqs as $req ) {
365 $req->returnToUrl = $returnToUrl;
366 // @codeCoverageIgnoreStart
367 if ( $req->username !== null && $req->username !== '' ) {
368 if ( $guessUserName === null ) {
369 $guessUserName = $req->username;
370 } elseif ( $guessUserName !== $req->username ) {
371 $guessUserName = null;
372 break;
373 }
374 }
375 // @codeCoverageIgnoreEnd
376 }
377
378 // Check for special-case login of a just-created account
380 $reqs, CreatedAccountAuthenticationRequest::class
381 );
382 if ( $req ) {
383 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
384 throw new \LogicException(
385 'CreatedAccountAuthenticationRequests are only valid on ' .
386 'the same AuthManager that created the account'
387 );
388 }
389
390 $user = $this->userFactory->newFromName( (string)$req->username );
391 // @codeCoverageIgnoreStart
392 if ( !$user ) {
393 throw new \UnexpectedValueException(
394 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
395 );
396 } elseif ( $user->getId() != $req->id ) {
397 throw new \UnexpectedValueException(
398 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
399 );
400 }
401 // @codeCoverageIgnoreEnd
402
403 $this->logger->info( 'Logging in {user} after account creation', [
404 'user' => $user->getName(),
405 ] );
406 $ret = AuthenticationResponse::newPass( $user->getName() );
407 $this->setSessionDataForUser( $user );
408 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
409 $session->remove( 'AuthManager::authnState' );
410 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
411 $ret, $user, $user->getName(), [] );
412 return $ret;
413 }
414
415 $this->removeAuthenticationSessionData( null );
416
417 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
418 $status = $provider->testForAuthentication( $reqs );
419 if ( !$status->isGood() ) {
420 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
422 Status::wrap( $status )->getMessage()
423 );
424 $this->callMethodOnProviders( 7, 'postAuthentication',
425 [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
426 );
427 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit( $ret, null, $guessUserName, [] );
428 return $ret;
429 }
430 }
431
432 $state = [
433 'reqs' => $reqs,
434 'returnToUrl' => $returnToUrl,
435 'guessUserName' => $guessUserName,
436 'primary' => null,
437 'primaryResponse' => null,
438 'secondary' => [],
439 'maybeLink' => [],
440 'continueRequests' => [],
441 ];
442
443 // Preserve state from a previous failed login
445 $reqs, CreateFromLoginAuthenticationRequest::class
446 );
447 if ( $req ) {
448 $state['maybeLink'] = $req->maybeLink;
449 }
450
451 $session = $this->request->getSession();
452 $session->setSecret( 'AuthManager::authnState', $state );
453 $session->persist();
454
455 return $this->continueAuthentication( $reqs );
456 }
457
480 public function continueAuthentication( array $reqs ) {
481 $session = $this->request->getSession();
482 try {
483 if ( !$session->canSetUser() ) {
484 // Caller should have called canAuthenticateNow()
485 // @codeCoverageIgnoreStart
486 throw new \LogicException( 'Authentication is not possible now' );
487 // @codeCoverageIgnoreEnd
488 }
489
490 $state = $session->getSecret( 'AuthManager::authnState' );
491 if ( !is_array( $state ) ) {
493 wfMessage( 'authmanager-authn-not-in-progress' )
494 );
495 }
496 $state['continueRequests'] = [];
497
498 $guessUserName = $state['guessUserName'];
499
500 foreach ( $reqs as $req ) {
501 $req->returnToUrl = $state['returnToUrl'];
502 }
503
504 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
505
506 if ( $state['primary'] === null ) {
507 // We haven't picked a PrimaryAuthenticationProvider yet
508 // @codeCoverageIgnoreStart
509 $guessUserName = null;
510 foreach ( $reqs as $req ) {
511 if ( $req->username !== null && $req->username !== '' ) {
512 if ( $guessUserName === null ) {
513 $guessUserName = $req->username;
514 } elseif ( $guessUserName !== $req->username ) {
515 $guessUserName = null;
516 break;
517 }
518 }
519 }
520 $state['guessUserName'] = $guessUserName;
521 // @codeCoverageIgnoreEnd
522 $state['reqs'] = $reqs;
523
524 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
525 $res = $provider->beginPrimaryAuthentication( $reqs );
526 switch ( $res->status ) {
528 $state['primary'] = $id;
529 $state['primaryResponse'] = $res;
530 $this->logger->debug( "Primary login with $id succeeded" );
531 break 2;
533 $this->logger->debug( "Login failed in primary authentication by $id" );
534 if ( $res->createRequest || $state['maybeLink'] ) {
535 $res->createRequest = new CreateFromLoginAuthenticationRequest(
536 $res->createRequest, $state['maybeLink']
537 );
538 }
540 7,
541 'postAuthentication',
542 [
543 $this->userFactory->newFromName( (string)$guessUserName ),
544 $res
545 ]
546 );
547 $session->remove( 'AuthManager::authnState' );
548 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
549 $res, null, $guessUserName, [] );
550 return $res;
552 // Continue loop
553 break;
556 $this->logger->debug( "Primary login with $id returned $res->status" );
557 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
558 $state['primary'] = $id;
559 $state['continueRequests'] = $res->neededRequests;
560 $session->setSecret( 'AuthManager::authnState', $state );
561 return $res;
562
563 // @codeCoverageIgnoreStart
564 default:
565 throw new \DomainException(
566 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
567 );
568 // @codeCoverageIgnoreEnd
569 }
570 }
571 if ( $state['primary'] === null ) {
572 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
574 wfMessage( 'authmanager-authn-no-primary' )
575 );
576 $this->callMethodOnProviders( 7, 'postAuthentication',
577 [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
578 );
579 $session->remove( 'AuthManager::authnState' );
580 return $ret;
581 }
582 } elseif ( $state['primaryResponse'] === null ) {
583 $provider = $this->getAuthenticationProvider( $state['primary'] );
584 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
585 // Configuration changed? Force them to start over.
586 // @codeCoverageIgnoreStart
588 wfMessage( 'authmanager-authn-not-in-progress' )
589 );
590 $this->callMethodOnProviders( 7, 'postAuthentication',
591 [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
592 );
593 $session->remove( 'AuthManager::authnState' );
594 return $ret;
595 // @codeCoverageIgnoreEnd
596 }
597 $id = $provider->getUniqueId();
598 $res = $provider->continuePrimaryAuthentication( $reqs );
599 switch ( $res->status ) {
601 $state['primaryResponse'] = $res;
602 $this->logger->debug( "Primary login with $id succeeded" );
603 break;
605 $this->logger->debug( "Login failed in primary authentication by $id" );
606 if ( $res->createRequest || $state['maybeLink'] ) {
607 $res->createRequest = new CreateFromLoginAuthenticationRequest(
608 $res->createRequest, $state['maybeLink']
609 );
610 }
611 $this->callMethodOnProviders( 7, 'postAuthentication',
612 [ $this->userFactory->newFromName( (string)$guessUserName ), $res ]
613 );
614 $session->remove( 'AuthManager::authnState' );
615 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
616 $res, null, $guessUserName, [] );
617 return $res;
620 $this->logger->debug( "Primary login with $id returned $res->status" );
621 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
622 $state['continueRequests'] = $res->neededRequests;
623 $session->setSecret( 'AuthManager::authnState', $state );
624 return $res;
625 default:
626 throw new \DomainException(
627 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
628 );
629 }
630 }
631
632 $res = $state['primaryResponse'];
633 if ( $res->username === null ) {
634 $provider = $this->getAuthenticationProvider( $state['primary'] );
635 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
636 // Configuration changed? Force them to start over.
637 // @codeCoverageIgnoreStart
639 wfMessage( 'authmanager-authn-not-in-progress' )
640 );
641 $this->callMethodOnProviders( 7, 'postAuthentication',
642 [ $this->userFactory->newFromName( (string)$guessUserName ), $ret ]
643 );
644 $session->remove( 'AuthManager::authnState' );
645 return $ret;
646 // @codeCoverageIgnoreEnd
647 }
648
649 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
650 $res->linkRequest &&
651 // don't confuse the user with an incorrect message if linking is disabled
652 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
653 ) {
654 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
655 $msg = 'authmanager-authn-no-local-user-link';
656 } else {
657 $msg = 'authmanager-authn-no-local-user';
658 }
659 $this->logger->debug(
660 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
661 );
663 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
664 self::ACTION_LOGIN,
665 [],
667 );
668 if ( $res->createRequest || $state['maybeLink'] ) {
669 $ret->createRequest = new CreateFromLoginAuthenticationRequest(
670 $res->createRequest, $state['maybeLink']
671 );
672 $ret->neededRequests[] = $ret->createRequest;
673 }
674 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
675 $session->setSecret( 'AuthManager::authnState', [
676 'reqs' => [], // Will be filled in later
677 'primary' => null,
678 'primaryResponse' => null,
679 'secondary' => [],
680 'continueRequests' => $ret->neededRequests,
681 ] + $state );
682 return $ret;
683 }
684
685 // Step 2: Primary authentication succeeded, create the User object
686 // (and add the user locally if necessary)
687
688 $user = $this->userFactory->newFromName(
689 (string)$res->username,
690 UserFactory::RIGOR_USABLE
691 );
692 if ( !$user ) {
693 $provider = $this->getAuthenticationProvider( $state['primary'] );
694 throw new \DomainException(
695 get_class( $provider ) . " returned an invalid username: {$res->username}"
696 );
697 }
698 if ( $user->getId() === 0 ) {
699 // User doesn't exist locally. Create it.
700 $this->logger->info( 'Auto-creating {user} on login', [
701 'user' => $user->getName(),
702 ] );
703 $status = $this->autoCreateUser( $user, $state['primary'], false );
704 if ( !$status->isGood() ) {
706 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
707 );
708 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
709 $session->remove( 'AuthManager::authnState' );
710 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
711 $ret, $user, $user->getName(), [] );
712 return $ret;
713 }
714 }
715
716 // Step 3: Iterate over all the secondary authentication providers.
717
718 $beginReqs = $state['reqs'];
719
720 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
721 if ( !isset( $state['secondary'][$id] ) ) {
722 // This provider isn't started yet, so we pass it the set
723 // of reqs from beginAuthentication instead of whatever
724 // might have been used by a previous provider in line.
725 $func = 'beginSecondaryAuthentication';
726 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
727 } elseif ( !$state['secondary'][$id] ) {
728 $func = 'continueSecondaryAuthentication';
729 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
730 } else {
731 continue;
732 }
733 switch ( $res->status ) {
735 $this->logger->debug( "Secondary login with $id succeeded" );
736 // fall through
738 $state['secondary'][$id] = true;
739 break;
741 $this->logger->debug( "Login failed in secondary authentication by $id" );
742 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
743 $session->remove( 'AuthManager::authnState' );
744 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
745 $res, $user, $user->getName(), [] );
746 return $res;
749 $this->logger->debug( "Secondary login with $id returned " . $res->status );
750 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
751 $state['secondary'][$id] = false;
752 $state['continueRequests'] = $res->neededRequests;
753 $session->setSecret( 'AuthManager::authnState', $state );
754 return $res;
755
756 // @codeCoverageIgnoreStart
757 default:
758 throw new \DomainException(
759 get_class( $provider ) . "::{$func}() returned $res->status"
760 );
761 // @codeCoverageIgnoreEnd
762 }
763 }
764
765 // Step 4: Authentication complete! Set the user in the session and
766 // clean up.
767
768 $this->logger->info( 'Login for {user} succeeded from {clientip}', [
769 'user' => $user->getName(),
770 'clientip' => $this->request->getIP(),
771 ] );
772 $rememberMeConfig = $this->config->get( 'RememberMe' );
773 if ( $rememberMeConfig === RememberMeAuthenticationRequest::ALWAYS_REMEMBER ) {
774 $rememberMe = true;
775 } elseif ( $rememberMeConfig === RememberMeAuthenticationRequest::NEVER_REMEMBER ) {
776 $rememberMe = false;
777 } else {
780 $beginReqs, RememberMeAuthenticationRequest::class
781 );
782 $rememberMe = $req && $req->rememberMe;
783 }
784 $this->setSessionDataForUser( $user, $rememberMe );
785 $ret = AuthenticationResponse::newPass( $user->getName() );
786 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
787 $session->remove( 'AuthManager::authnState' );
788 $this->removeAuthenticationSessionData( null );
789 $this->getHookRunner()->onAuthManagerLoginAuthenticateAudit(
790 $ret, $user, $user->getName(), [] );
791 return $ret;
792 } catch ( \Exception $ex ) {
793 $session->remove( 'AuthManager::authnState' );
794 throw $ex;
795 }
796 }
797
809 public function securitySensitiveOperationStatus( $operation ) {
810 $status = self::SEC_OK;
811
812 $this->logger->debug( __METHOD__ . ": Checking $operation" );
813
814 $session = $this->request->getSession();
815 $aId = $session->getUser()->getId();
816 if ( $aId === 0 ) {
817 // User isn't authenticated. DWIM?
819 $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
820 return $status;
821 }
822
823 if ( $session->canSetUser() ) {
824 $id = $session->get( 'AuthManager:lastAuthId' );
825 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
826 if ( $id !== $aId || $last === null ) {
827 $timeSinceLogin = PHP_INT_MAX; // Forever ago
828 } else {
829 $timeSinceLogin = max( 0, time() - $last );
830 }
831
832 $thresholds = $this->config->get( 'ReauthenticateTime' );
833 if ( isset( $thresholds[$operation] ) ) {
834 $threshold = $thresholds[$operation];
835 } elseif ( isset( $thresholds['default'] ) ) {
836 $threshold = $thresholds['default'];
837 } else {
838 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
839 }
840
841 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
842 $status = self::SEC_REAUTH;
843 }
844 } else {
845 $timeSinceLogin = -1;
846
847 $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
848 if ( isset( $pass[$operation] ) ) {
849 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
850 } elseif ( isset( $pass['default'] ) ) {
851 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
852 } else {
853 throw new \UnexpectedValueException(
854 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
855 );
856 }
857 }
858
859 $this->getHookRunner()->onSecuritySensitiveOperationStatus(
860 $status, $operation, $session, $timeSinceLogin );
861
862 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
863 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
864 $status = self::SEC_FAIL;
865 }
866
867 $this->logger->info( __METHOD__ . ": $operation is $status for '{user}'",
868 [
869 'user' => $session->getUser()->getName(),
870 'clientip' => $this->getRequest()->getIP(),
871 ]
872 );
873
874 return $status;
875 }
876
886 public function userCanAuthenticate( $username ) {
887 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
888 if ( $provider->testUserCanAuthenticate( $username ) ) {
889 return true;
890 }
891 }
892 return false;
893 }
894
909 public function normalizeUsername( $username ) {
910 $ret = [];
911 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
912 $normalized = $provider->providerNormalizeUsername( $username );
913 if ( $normalized !== null ) {
914 $ret[$normalized] = true;
915 }
916 }
917 return array_keys( $ret );
918 }
919
920 // endregion -- end of Authentication
921
922 /***************************************************************************/
923 // region Authentication data changing
933 public function revokeAccessForUser( $username ) {
934 $this->logger->info( 'Revoking access for {user}', [
935 'user' => $username,
936 ] );
937 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
938 }
939
949 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
950 $any = false;
951 $providers = $this->getPrimaryAuthenticationProviders() +
953
954 foreach ( $providers as $provider ) {
955 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
956 if ( !$status->isGood() ) {
957 // If status is not good because reset email password last attempt was within
958 // $wgPasswordReminderResendTime then return good status with throttled-mailpassword value;
959 // otherwise, return the $status wrapped.
960 return $status->hasMessage( 'throttled-mailpassword' )
961 ? Status::newGood( 'throttled-mailpassword' )
962 : Status::wrap( $status );
963 }
964 $any = $any || $status->value !== 'ignored';
965 }
966 if ( !$any ) {
967 return Status::newGood( 'ignored' )
968 ->warning( 'authmanager-change-not-supported' );
969 }
970 return Status::newGood();
971 }
972
990 public function changeAuthenticationData( AuthenticationRequest $req, $isAddition = false ) {
991 $this->logger->info( 'Changing authentication data for {user} class {what}', [
992 'user' => is_string( $req->username ) ? $req->username : '<no name>',
993 'what' => get_class( $req ),
994 ] );
995
996 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
997
998 // When the main account's authentication data is changed, invalidate
999 // all BotPasswords too.
1000 if ( !$isAddition ) {
1001 $this->botPasswordStore->invalidateUserPasswords( (string)$req->username );
1002 }
1003 }
1004
1005 // endregion -- end of Authentication data changing
1006
1007 /***************************************************************************/
1008 // region Account creation
1015 public function canCreateAccounts() {
1016 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1017 switch ( $provider->accountCreationType() ) {
1020 return true;
1021 }
1022 }
1023 return false;
1024 }
1025
1034 public function canCreateAccount( $username, $options = [] ) {
1035 // Back compat
1036 if ( is_int( $options ) ) {
1037 $options = [ 'flags' => $options ];
1038 }
1039 $options += [
1040 'flags' => User::READ_NORMAL,
1041 'creating' => false,
1042 ];
1043 $flags = $options['flags'];
1044
1045 if ( !$this->canCreateAccounts() ) {
1046 return Status::newFatal( 'authmanager-create-disabled' );
1047 }
1048
1049 if ( $this->userExists( $username, $flags ) ) {
1050 return Status::newFatal( 'userexists' );
1051 }
1052
1053 $user = $this->userFactory->newFromName( (string)$username, UserFactory::RIGOR_CREATABLE );
1054 if ( !is_object( $user ) ) {
1055 return Status::newFatal( 'noname' );
1056 } else {
1057 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
1058 if ( $user->getId() !== 0 ) {
1059 return Status::newFatal( 'userexists' );
1060 }
1061 }
1062
1063 // Denied by providers?
1064 $providers = $this->getPreAuthenticationProviders() +
1067 foreach ( $providers as $provider ) {
1068 $status = $provider->testUserForCreation( $user, false, $options );
1069 if ( !$status->isGood() ) {
1070 return Status::wrap( $status );
1071 }
1072 }
1073
1074 return Status::newGood();
1075 }
1076
1082 public function checkAccountCreatePermissions( Authority $creator ) {
1083 // Wiki is read-only?
1084 if ( $this->readOnlyMode->isReadOnly() ) {
1085 return Status::newFatal( wfMessage( 'readonlytext', $this->readOnlyMode->getReason() ) );
1086 }
1087
1088 $permStatus = new PermissionStatus();
1089 if ( !$creator->authorizeWrite(
1090 'createaccount',
1091 SpecialPage::getTitleFor( 'CreateAccount' ),
1092 $permStatus
1093 ) ) {
1094 return Status::wrap( $permStatus );
1095 }
1096
1097 $ip = $this->getRequest()->getIP();
1098 if ( $this->blockManager->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
1099 return Status::newFatal( 'sorbs_create_account_reason' );
1100 }
1101
1102 return Status::newGood();
1103 }
1104
1124 public function beginAccountCreation( Authority $creator, array $reqs, $returnToUrl ) {
1125 $session = $this->request->getSession();
1126 if ( !$this->canCreateAccounts() ) {
1127 // Caller should have called canCreateAccounts()
1128 $session->remove( 'AuthManager::accountCreationState' );
1129 throw new \LogicException( 'Account creation is not possible' );
1130 }
1131
1132 try {
1134 } catch ( \UnexpectedValueException $ex ) {
1135 $username = null;
1136 }
1137 if ( $username === null ) {
1138 $this->logger->debug( __METHOD__ . ': No username provided' );
1139 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1140 }
1141
1142 // Permissions check
1143 $status = $this->checkAccountCreatePermissions( $creator );
1144 if ( !$status->isGood() ) {
1145 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1146 'user' => $username,
1147 'creator' => $creator->getUser()->getName(),
1148 'reason' => $status->getWikiText( null, null, 'en' )
1149 ] );
1150 return AuthenticationResponse::newFail( $status->getMessage() );
1151 }
1152
1153 $status = $this->canCreateAccount(
1154 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1155 );
1156 if ( !$status->isGood() ) {
1157 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1158 'user' => $username,
1159 'creator' => $creator->getUser()->getName(),
1160 'reason' => $status->getWikiText( null, null, 'en' )
1161 ] );
1162 return AuthenticationResponse::newFail( $status->getMessage() );
1163 }
1164
1165 $user = $this->userFactory->newFromName( (string)$username, UserFactory::RIGOR_CREATABLE );
1166 foreach ( $reqs as $req ) {
1167 $req->username = $username;
1168 $req->returnToUrl = $returnToUrl;
1169 if ( $req instanceof UserDataAuthenticationRequest ) {
1170 $status = $req->populateUser( $user );
1171 if ( !$status->isGood() ) {
1172 $status = Status::wrap( $status );
1173 $session->remove( 'AuthManager::accountCreationState' );
1174 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1175 'user' => $user->getName(),
1176 'creator' => $creator->getUser()->getName(),
1177 'reason' => $status->getWikiText( null, null, 'en' ),
1178 ] );
1179 return AuthenticationResponse::newFail( $status->getMessage() );
1180 }
1181 }
1182 }
1183
1184 $this->removeAuthenticationSessionData( null );
1185
1186 $state = [
1187 'username' => $username,
1188 'userid' => 0,
1189 'creatorid' => $creator->getUser()->getId(),
1190 'creatorname' => $creator->getUser()->getName(),
1191 'reqs' => $reqs,
1192 'returnToUrl' => $returnToUrl,
1193 'primary' => null,
1194 'primaryResponse' => null,
1195 'secondary' => [],
1196 'continueRequests' => [],
1197 'maybeLink' => [],
1198 'ranPreTests' => false,
1199 ];
1200
1201 // Special case: converting a login to an account creation
1203 $reqs, CreateFromLoginAuthenticationRequest::class
1204 );
1205 if ( $req ) {
1206 $state['maybeLink'] = $req->maybeLink;
1207
1208 if ( $req->createRequest ) {
1209 $reqs[] = $req->createRequest;
1210 $state['reqs'][] = $req->createRequest;
1211 }
1212 }
1213
1214 $session->setSecret( 'AuthManager::accountCreationState', $state );
1215 $session->persist();
1216
1217 return $this->continueAccountCreation( $reqs );
1218 }
1219
1225 public function continueAccountCreation( array $reqs ) {
1226 $session = $this->request->getSession();
1227 try {
1228 if ( !$this->canCreateAccounts() ) {
1229 // Caller should have called canCreateAccounts()
1230 $session->remove( 'AuthManager::accountCreationState' );
1231 throw new \LogicException( 'Account creation is not possible' );
1232 }
1233
1234 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1235 if ( !is_array( $state ) ) {
1237 wfMessage( 'authmanager-create-not-in-progress' )
1238 );
1239 }
1240 $state['continueRequests'] = [];
1241
1242 // Step 0: Prepare and validate the input
1243
1244 $user = $this->userFactory->newFromName(
1245 (string)$state['username'],
1246 UserFactory::RIGOR_CREATABLE
1247 );
1248 if ( !is_object( $user ) ) {
1249 $session->remove( 'AuthManager::accountCreationState' );
1250 $this->logger->debug( __METHOD__ . ': Invalid username', [
1251 'user' => $state['username'],
1252 ] );
1253 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1254 }
1255
1256 if ( $state['creatorid'] ) {
1257 $creator = $this->userFactory->newFromId( (int)$state['creatorid'] );
1258 } else {
1259 $creator = $this->userFactory->newAnonymous();
1260 $creator->setName( $state['creatorname'] );
1261 }
1262
1263 // Avoid account creation races on double submissions
1264 $cache = \ObjectCache::getLocalClusterInstance();
1265 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1266 if ( !$lock ) {
1267 // Don't clear AuthManager::accountCreationState for this code
1268 // path because the process that won the race owns it.
1269 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1270 'user' => $user->getName(),
1271 'creator' => $creator->getName(),
1272 ] );
1273 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1274 }
1275
1276 // Permissions check
1277 $status = $this->checkAccountCreatePermissions( $creator );
1278 if ( !$status->isGood() ) {
1279 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1280 'user' => $user->getName(),
1281 'creator' => $creator->getName(),
1282 'reason' => $status->getWikiText( null, null, 'en' )
1283 ] );
1284 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1285 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1286 $session->remove( 'AuthManager::accountCreationState' );
1287 return $ret;
1288 }
1289
1290 // Load from primary DB for existence check
1291 $user->load( User::READ_LOCKING );
1292
1293 if ( $state['userid'] === 0 ) {
1294 if ( $user->getId() !== 0 ) {
1295 $this->logger->debug( __METHOD__ . ': User exists locally', [
1296 'user' => $user->getName(),
1297 'creator' => $creator->getName(),
1298 ] );
1299 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1300 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1301 $session->remove( 'AuthManager::accountCreationState' );
1302 return $ret;
1303 }
1304 } else {
1305 if ( $user->getId() === 0 ) {
1306 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1307 'user' => $user->getName(),
1308 'creator' => $creator->getName(),
1309 'expected_id' => $state['userid'],
1310 ] );
1311 throw new \UnexpectedValueException(
1312 "User \"{$state['username']}\" should exist now, but doesn't!"
1313 );
1314 }
1315 if ( $user->getId() !== $state['userid'] ) {
1316 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1317 'user' => $user->getName(),
1318 'creator' => $creator->getName(),
1319 'expected_id' => $state['userid'],
1320 'actual_id' => $user->getId(),
1321 ] );
1322 throw new \UnexpectedValueException(
1323 "User \"{$state['username']}\" exists, but " .
1324 "ID {$user->getId()} !== {$state['userid']}!"
1325 );
1326 }
1327 }
1328 foreach ( $state['reqs'] as $req ) {
1329 if ( $req instanceof UserDataAuthenticationRequest ) {
1330 $status = $req->populateUser( $user );
1331 if ( !$status->isGood() ) {
1332 // This should never happen...
1333 $status = Status::wrap( $status );
1334 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1335 'user' => $user->getName(),
1336 'creator' => $creator->getName(),
1337 'reason' => $status->getWikiText( null, null, 'en' ),
1338 ] );
1339 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1340 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1341 $session->remove( 'AuthManager::accountCreationState' );
1342 return $ret;
1343 }
1344 }
1345 }
1346
1347 foreach ( $reqs as $req ) {
1348 $req->returnToUrl = $state['returnToUrl'];
1349 $req->username = $state['username'];
1350 }
1351
1352 // Run pre-creation tests, if we haven't already
1353 if ( !$state['ranPreTests'] ) {
1354 $providers = $this->getPreAuthenticationProviders() +
1357 foreach ( $providers as $id => $provider ) {
1358 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1359 if ( !$status->isGood() ) {
1360 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1361 'user' => $user->getName(),
1362 'creator' => $creator->getName(),
1363 ] );
1365 Status::wrap( $status )->getMessage()
1366 );
1367 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1368 $session->remove( 'AuthManager::accountCreationState' );
1369 return $ret;
1370 }
1371 }
1372
1373 $state['ranPreTests'] = true;
1374 }
1375
1376 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1377
1378 if ( $state['primary'] === null ) {
1379 // We haven't picked a PrimaryAuthenticationProvider yet
1380 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1381 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1382 continue;
1383 }
1384 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1385 switch ( $res->status ) {
1387 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1388 'user' => $user->getName(),
1389 'creator' => $creator->getName(),
1390 ] );
1391 $state['primary'] = $id;
1392 $state['primaryResponse'] = $res;
1393 break 2;
1395 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1396 'user' => $user->getName(),
1397 'creator' => $creator->getName(),
1398 ] );
1399 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1400 $session->remove( 'AuthManager::accountCreationState' );
1401 return $res;
1403 // Continue loop
1404 break;
1407 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1408 'user' => $user->getName(),
1409 'creator' => $creator->getName(),
1410 ] );
1411 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1412 $state['primary'] = $id;
1413 $state['continueRequests'] = $res->neededRequests;
1414 $session->setSecret( 'AuthManager::accountCreationState', $state );
1415 return $res;
1416
1417 // @codeCoverageIgnoreStart
1418 default:
1419 throw new \DomainException(
1420 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1421 );
1422 // @codeCoverageIgnoreEnd
1423 }
1424 }
1425 if ( $state['primary'] === null ) {
1426 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1427 'user' => $user->getName(),
1428 'creator' => $creator->getName(),
1429 ] );
1431 wfMessage( 'authmanager-create-no-primary' )
1432 );
1433 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1434 $session->remove( 'AuthManager::accountCreationState' );
1435 return $ret;
1436 }
1437 } elseif ( $state['primaryResponse'] === null ) {
1438 $provider = $this->getAuthenticationProvider( $state['primary'] );
1439 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1440 // Configuration changed? Force them to start over.
1441 // @codeCoverageIgnoreStart
1443 wfMessage( 'authmanager-create-not-in-progress' )
1444 );
1445 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1446 $session->remove( 'AuthManager::accountCreationState' );
1447 return $ret;
1448 // @codeCoverageIgnoreEnd
1449 }
1450 $id = $provider->getUniqueId();
1451 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1452 switch ( $res->status ) {
1454 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1455 'user' => $user->getName(),
1456 'creator' => $creator->getName(),
1457 ] );
1458 $state['primaryResponse'] = $res;
1459 break;
1461 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1462 'user' => $user->getName(),
1463 'creator' => $creator->getName(),
1464 ] );
1465 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1466 $session->remove( 'AuthManager::accountCreationState' );
1467 return $res;
1470 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1471 'user' => $user->getName(),
1472 'creator' => $creator->getName(),
1473 ] );
1474 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1475 $state['continueRequests'] = $res->neededRequests;
1476 $session->setSecret( 'AuthManager::accountCreationState', $state );
1477 return $res;
1478 default:
1479 throw new \DomainException(
1480 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1481 );
1482 }
1483 }
1484
1485 // Step 2: Primary authentication succeeded, create the User object
1486 // and add the user locally.
1487
1488 if ( $state['userid'] === 0 ) {
1489 $this->logger->info( 'Creating user {user} during account creation', [
1490 'user' => $user->getName(),
1491 'creator' => $creator->getName(),
1492 ] );
1493 $status = $user->addToDatabase();
1494 if ( !$status->isOK() ) {
1495 // @codeCoverageIgnoreStart
1496 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1497 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1498 $session->remove( 'AuthManager::accountCreationState' );
1499 return $ret;
1500 // @codeCoverageIgnoreEnd
1501 }
1502 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1503 $this->getHookRunner()->onLocalUserCreated( $user, false );
1504 $user->saveSettings();
1505 $state['userid'] = $user->getId();
1506
1507 // Update user count
1508 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1509
1510 // Watch user's userpage and talk page
1511 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
1512
1513 // Inform the provider
1514 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1515
1516 // Log the creation
1517 if ( $this->config->get( 'NewUserLog' ) ) {
1518 $isAnon = $creator->isAnon();
1519 $logEntry = new \ManualLogEntry(
1520 'newusers',
1521 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1522 );
1523 $logEntry->setPerformer( $isAnon ? $user : $creator );
1524 $logEntry->setTarget( $user->getUserPage() );
1527 $state['reqs'], CreationReasonAuthenticationRequest::class
1528 );
1529 $logEntry->setComment( $req ? $req->reason : '' );
1530 $logEntry->setParameters( [
1531 '4::userid' => $user->getId(),
1532 ] );
1533 $logid = $logEntry->insert();
1534 $logEntry->publish( $logid );
1535 }
1536 }
1537
1538 // Step 3: Iterate over all the secondary authentication providers.
1539
1540 $beginReqs = $state['reqs'];
1541
1542 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1543 if ( !isset( $state['secondary'][$id] ) ) {
1544 // This provider isn't started yet, so we pass it the set
1545 // of reqs from beginAuthentication instead of whatever
1546 // might have been used by a previous provider in line.
1547 $func = 'beginSecondaryAccountCreation';
1548 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1549 } elseif ( !$state['secondary'][$id] ) {
1550 $func = 'continueSecondaryAccountCreation';
1551 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1552 } else {
1553 continue;
1554 }
1555 switch ( $res->status ) {
1557 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1558 'user' => $user->getName(),
1559 'creator' => $creator->getName(),
1560 ] );
1561 // fall through
1563 $state['secondary'][$id] = true;
1564 break;
1567 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1568 'user' => $user->getName(),
1569 'creator' => $creator->getName(),
1570 ] );
1571 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1572 $state['secondary'][$id] = false;
1573 $state['continueRequests'] = $res->neededRequests;
1574 $session->setSecret( 'AuthManager::accountCreationState', $state );
1575 return $res;
1577 throw new \DomainException(
1578 get_class( $provider ) . "::{$func}() returned $res->status." .
1579 ' Secondary providers are not allowed to fail account creation, that' .
1580 ' should have been done via testForAccountCreation().'
1581 );
1582 // @codeCoverageIgnoreStart
1583 default:
1584 throw new \DomainException(
1585 get_class( $provider ) . "::{$func}() returned $res->status"
1586 );
1587 // @codeCoverageIgnoreEnd
1588 }
1589 }
1590
1591 $id = $user->getId();
1592 $name = $user->getName();
1593 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1594 $ret = AuthenticationResponse::newPass( $name );
1595 $ret->loginRequest = $req;
1596 $this->createdAccountAuthenticationRequests[] = $req;
1597
1598 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1599 'user' => $user->getName(),
1600 'creator' => $creator->getName(),
1601 ] );
1602
1603 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1604 $session->remove( 'AuthManager::accountCreationState' );
1605 $this->removeAuthenticationSessionData( null );
1606 return $ret;
1607 } catch ( \Exception $ex ) {
1608 $session->remove( 'AuthManager::accountCreationState' );
1609 throw $ex;
1610 }
1611 }
1612
1631 public function autoCreateUser( User $user, $source, $login = true, $log = true ) {
1632 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1633 $source !== self::AUTOCREATE_SOURCE_MAINT &&
1635 ) {
1636 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1637 }
1638
1639 $username = $user->getName();
1640
1641 // Try the local user from the replica DB
1642 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName( $username );
1643 $localId = ( $localUserIdentity && $localUserIdentity->getId() )
1644 ? $localUserIdentity->getId()
1645 : null;
1646 $flags = User::READ_NORMAL;
1647
1648 // Fetch the user ID from the primary, so that we don't try to create the user
1649 // when they already exist, due to replication lag
1650 // @codeCoverageIgnoreStart
1651 if (
1652 !$localId &&
1653 $this->loadBalancer->getReaderIndex() !== 0
1654 ) {
1655 $localUserIdentity = $this->userIdentityLookup->getUserIdentityByName(
1656 $username,
1657 UserIdentityLookup::READ_LATEST
1658 );
1659 $localId = ( $localUserIdentity && $localUserIdentity->getId() )
1660 ? $localUserIdentity->getId()
1661 : null;
1662 $flags = User::READ_LATEST;
1663 }
1664 // @codeCoverageIgnoreEnd
1665
1666 if ( $localId ) {
1667 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1668 'username' => $username,
1669 ] );
1670 $user->setId( $localId );
1671 $user->loadFromId( $flags );
1672 if ( $login ) {
1673 $this->setSessionDataForUser( $user );
1674 }
1675 return Status::newGood()->warning( 'userexists' );
1676 }
1677
1678 // Wiki is read-only?
1679 if ( $this->readOnlyMode->isReadOnly() ) {
1680 $reason = $this->readOnlyMode->getReason();
1681 $this->logger->debug( __METHOD__ . ': denied because of read only mode: {reason}', [
1682 'username' => $username,
1683 'reason' => $reason,
1684 ] );
1685 $user->setId( 0 );
1686 $user->loadFromId();
1687 return Status::newFatal( wfMessage( 'readonlytext', $reason ) );
1688 }
1689
1690 // Check the session, if we tried to create this user already there's
1691 // no point in retrying.
1692 $session = $this->request->getSession();
1693 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1694 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1695 'username' => $username,
1696 'sessionid' => $session->getId(),
1697 ] );
1698 $user->setId( 0 );
1699 $user->loadFromId();
1700 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1701 if ( $reason instanceof StatusValue ) {
1702 return Status::wrap( $reason );
1703 } else {
1704 return Status::newFatal( $reason );
1705 }
1706 }
1707
1708 // Is the username creatable?
1709 if ( !$this->userNameUtils->isCreatable( $username ) ) {
1710 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1711 'username' => $username,
1712 ] );
1713 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname' );
1714 $user->setId( 0 );
1715 $user->loadFromId();
1716 return Status::newFatal( 'noname' );
1717 }
1718
1719 // Is the IP user able to create accounts?
1720 $anon = $this->userFactory->newAnonymous();
1721 if ( $source !== self::AUTOCREATE_SOURCE_MAINT &&
1722 !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' )
1723 ) {
1724 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1725 'username' => $username,
1726 'clientip' => $anon->getName(),
1727 ] );
1728 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm' );
1729 $session->persist();
1730 $user->setId( 0 );
1731 $user->loadFromId();
1732 return Status::newFatal( 'authmanager-autocreate-noperm' );
1733 }
1734
1735 // Avoid account creation races on double submissions
1736 $cache = \ObjectCache::getLocalClusterInstance();
1737 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1738 if ( !$lock ) {
1739 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1740 'user' => $username,
1741 ] );
1742 $user->setId( 0 );
1743 $user->loadFromId();
1744 return Status::newFatal( 'usernameinprogress' );
1745 }
1746
1747 // Denied by providers?
1748 $options = [
1749 'flags' => User::READ_LATEST,
1750 'creating' => true,
1751 ];
1752 $providers = $this->getPreAuthenticationProviders() +
1755 foreach ( $providers as $provider ) {
1756 $status = $provider->testUserForCreation( $user, $source, $options );
1757 if ( !$status->isGood() ) {
1758 $ret = Status::wrap( $status );
1759 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1760 'username' => $username,
1761 'reason' => $ret->getWikiText( null, null, 'en' ),
1762 ] );
1763 $session->set( 'AuthManager::AutoCreateBlacklist', $status );
1764 $user->setId( 0 );
1765 $user->loadFromId();
1766 return $ret;
1767 }
1768 }
1769
1770 $backoffKey = $cache->makeKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1771 if ( $cache->get( $backoffKey ) ) {
1772 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1773 'username' => $username,
1774 ] );
1775 $user->setId( 0 );
1776 $user->loadFromId();
1777 return Status::newFatal( 'authmanager-autocreate-exception' );
1778 }
1779
1780 // Checks passed, create the user...
1781 $from = $_SERVER['REQUEST_URI'] ?? 'CLI';
1782 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1783 'username' => $username,
1784 'from' => $from,
1785 ] );
1786
1787 // Ignore warnings about primary connections/writes...hard to avoid here
1788 $trxProfilerSilencedScope = \Profiler::instance()->getTransactionProfiler()->silenceForScope();
1789 try {
1790 $status = $user->addToDatabase();
1791 if ( !$status->isOK() ) {
1792 // Double-check for a race condition (T70012). We make use of the fact that when
1793 // addToDatabase fails due to the user already existing, the user object gets loaded.
1794 if ( $user->getId() ) {
1795 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1796 'username' => $username,
1797 ] );
1798 if ( $login ) {
1799 $this->setSessionDataForUser( $user );
1800 }
1801 $status = Status::newGood()->warning( 'userexists' );
1802 } else {
1803 $this->logger->error( __METHOD__ . ': {username} failed with message {msg}', [
1804 'username' => $username,
1805 'msg' => $status->getWikiText( null, null, 'en' )
1806 ] );
1807 $user->setId( 0 );
1808 $user->loadFromId();
1809 }
1810 return $status;
1811 }
1812 } catch ( \Exception $ex ) {
1813 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1814 'username' => $username,
1815 'exception' => $ex,
1816 ] );
1817 // Do not keep throwing errors for a while
1818 $cache->set( $backoffKey, 1, 600 );
1819 // Bubble up error; which should normally trigger DB rollbacks
1820 throw $ex;
1821 }
1822
1823 $this->setDefaultUserOptions( $user, false );
1824
1825 // Inform the providers
1826 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1827
1828 $this->getHookRunner()->onLocalUserCreated( $user, true );
1829 $user->saveSettings();
1830
1831 // Update user count
1832 \DeferredUpdates::addUpdate( \SiteStatsUpdate::factory( [ 'users' => 1 ] ) );
1833 // Watch user's userpage and talk page
1834 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1835 $this->watchlistManager->addWatchIgnoringRights( $user, $user->getUserPage() );
1836 } );
1837
1838 // Log the creation
1839 if ( $this->config->get( 'NewUserLog' ) && $log ) {
1840 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1841 $logEntry->setPerformer( $user );
1842 $logEntry->setTarget( $user->getUserPage() );
1843 $logEntry->setComment( '' );
1844 $logEntry->setParameters( [
1845 '4::userid' => $user->getId(),
1846 ] );
1847 $logEntry->insert();
1848 }
1849
1850 ScopedCallback::consume( $trxProfilerSilencedScope );
1851
1852 if ( $login ) {
1853 $this->setSessionDataForUser( $user );
1854 }
1855
1856 return Status::newGood();
1857 }
1858
1859 // endregion -- end of Account creation
1860
1861 /***************************************************************************/
1862 // region Account linking
1869 public function canLinkAccounts() {
1870 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1871 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1872 return true;
1873 }
1874 }
1875 return false;
1876 }
1877
1887 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1888 $session = $this->request->getSession();
1889 $session->remove( 'AuthManager::accountLinkState' );
1890
1891 if ( !$this->canLinkAccounts() ) {
1892 // Caller should have called canLinkAccounts()
1893 throw new \LogicException( 'Account linking is not possible' );
1894 }
1895
1896 if ( $user->getId() === 0 ) {
1897 if ( !$this->userNameUtils->isUsable( $user->getName() ) ) {
1898 $msg = wfMessage( 'noname' );
1899 } else {
1900 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1901 }
1902 return AuthenticationResponse::newFail( $msg );
1903 }
1904 foreach ( $reqs as $req ) {
1905 $req->username = $user->getName();
1906 $req->returnToUrl = $returnToUrl;
1907 }
1908
1909 $this->removeAuthenticationSessionData( null );
1910
1911 $providers = $this->getPreAuthenticationProviders();
1912 foreach ( $providers as $id => $provider ) {
1913 $status = $provider->testForAccountLink( $user );
1914 if ( !$status->isGood() ) {
1915 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1916 'user' => $user->getName(),
1917 ] );
1919 Status::wrap( $status )->getMessage()
1920 );
1921 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1922 return $ret;
1923 }
1924 }
1925
1926 $state = [
1927 'username' => $user->getName(),
1928 'userid' => $user->getId(),
1929 'returnToUrl' => $returnToUrl,
1930 'primary' => null,
1931 'continueRequests' => [],
1932 ];
1933
1934 $providers = $this->getPrimaryAuthenticationProviders();
1935 foreach ( $providers as $id => $provider ) {
1936 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1937 continue;
1938 }
1939
1940 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1941 switch ( $res->status ) {
1943 $this->logger->info( "Account linked to {user} by $id", [
1944 'user' => $user->getName(),
1945 ] );
1946 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1947 return $res;
1948
1950 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1951 'user' => $user->getName(),
1952 ] );
1953 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1954 return $res;
1955
1957 // Continue loop
1958 break;
1959
1962 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1963 'user' => $user->getName(),
1964 ] );
1965 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1966 $state['primary'] = $id;
1967 $state['continueRequests'] = $res->neededRequests;
1968 $session->setSecret( 'AuthManager::accountLinkState', $state );
1969 $session->persist();
1970 return $res;
1971
1972 // @codeCoverageIgnoreStart
1973 default:
1974 throw new \DomainException(
1975 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1976 );
1977 // @codeCoverageIgnoreEnd
1978 }
1979 }
1980
1981 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1982 'user' => $user->getName(),
1983 ] );
1985 wfMessage( 'authmanager-link-no-primary' )
1986 );
1987 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1988 return $ret;
1989 }
1990
1996 public function continueAccountLink( array $reqs ) {
1997 $session = $this->request->getSession();
1998 try {
1999 if ( !$this->canLinkAccounts() ) {
2000 // Caller should have called canLinkAccounts()
2001 $session->remove( 'AuthManager::accountLinkState' );
2002 throw new \LogicException( 'Account linking is not possible' );
2003 }
2004
2005 $state = $session->getSecret( 'AuthManager::accountLinkState' );
2006 if ( !is_array( $state ) ) {
2008 wfMessage( 'authmanager-link-not-in-progress' )
2009 );
2010 }
2011 $state['continueRequests'] = [];
2012
2013 // Step 0: Prepare and validate the input
2014
2015 $user = $this->userFactory->newFromName(
2016 (string)$state['username'],
2017 UserFactory::RIGOR_USABLE
2018 );
2019 if ( !is_object( $user ) ) {
2020 $session->remove( 'AuthManager::accountLinkState' );
2021 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
2022 }
2023 if ( $user->getId() !== $state['userid'] ) {
2024 throw new \UnexpectedValueException(
2025 "User \"{$state['username']}\" is valid, but " .
2026 "ID {$user->getId()} !== {$state['userid']}!"
2027 );
2028 }
2029
2030 foreach ( $reqs as $req ) {
2031 $req->username = $state['username'];
2032 $req->returnToUrl = $state['returnToUrl'];
2033 }
2034
2035 // Step 1: Call the primary again until it succeeds
2036
2037 $provider = $this->getAuthenticationProvider( $state['primary'] );
2038 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
2039 // Configuration changed? Force them to start over.
2040 // @codeCoverageIgnoreStart
2042 wfMessage( 'authmanager-link-not-in-progress' )
2043 );
2044 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
2045 $session->remove( 'AuthManager::accountLinkState' );
2046 return $ret;
2047 // @codeCoverageIgnoreEnd
2048 }
2049 $id = $provider->getUniqueId();
2050 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
2051 switch ( $res->status ) {
2053 $this->logger->info( "Account linked to {user} by $id", [
2054 'user' => $user->getName(),
2055 ] );
2056 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
2057 $session->remove( 'AuthManager::accountLinkState' );
2058 return $res;
2060 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
2061 'user' => $user->getName(),
2062 ] );
2063 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
2064 $session->remove( 'AuthManager::accountLinkState' );
2065 return $res;
2068 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
2069 'user' => $user->getName(),
2070 ] );
2071 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
2072 $state['continueRequests'] = $res->neededRequests;
2073 $session->setSecret( 'AuthManager::accountLinkState', $state );
2074 return $res;
2075 default:
2076 throw new \DomainException(
2077 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
2078 );
2079 }
2080 } catch ( \Exception $ex ) {
2081 $session->remove( 'AuthManager::accountLinkState' );
2082 throw $ex;
2083 }
2084 }
2085
2086 // endregion -- end of Account linking
2087
2088 /***************************************************************************/
2089 // region Information methods
2110 public function getAuthenticationRequests( $action, UserIdentity $user = null ) {
2111 $options = [];
2112 $providerAction = $action;
2113
2114 // Figure out which providers to query
2115 switch ( $action ) {
2116 case self::ACTION_LOGIN:
2118 $providers = $this->getPreAuthenticationProviders() +
2121 break;
2122
2124 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
2125 return is_array( $state ) ? $state['continueRequests'] : [];
2126
2128 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
2129 return is_array( $state ) ? $state['continueRequests'] : [];
2130
2131 case self::ACTION_LINK:
2132 $providers = [];
2133 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2134 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2135 $providers[] = $p;
2136 }
2137 }
2138 break;
2139
2141 $providers = [];
2142 foreach ( $this->getPrimaryAuthenticationProviders() as $p ) {
2143 if ( $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
2144 $providers[] = $p;
2145 }
2146 }
2147
2148 // To providers, unlink and remove are identical.
2149 $providerAction = self::ACTION_REMOVE;
2150 break;
2151
2153 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
2154 return is_array( $state ) ? $state['continueRequests'] : [];
2155
2158 $providers = $this->getPrimaryAuthenticationProviders() +
2160 break;
2161
2162 // @codeCoverageIgnoreStart
2163 default:
2164 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2165 }
2166 // @codeCoverageIgnoreEnd
2167
2168 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2169 }
2170
2181 $providerAction, array $options, array $providers, UserIdentity $user = null
2182 ) {
2183 $user = $user ?: \RequestContext::getMain()->getUser();
2184 $options['username'] = $user->isRegistered() ? $user->getName() : null;
2185
2186 // Query them and merge results
2187 $reqs = [];
2188 foreach ( $providers as $provider ) {
2189 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2190 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2191 $id = $req->getUniqueId();
2192
2193 // If a required request if from a Primary, mark it as "primary-required" instead
2194 if ( $isPrimary && $req->required ) {
2196 }
2197
2198 if (
2199 !isset( $reqs[$id] )
2200 || $req->required === AuthenticationRequest::REQUIRED
2201 || $reqs[$id] === AuthenticationRequest::OPTIONAL
2202 ) {
2203 $reqs[$id] = $req;
2204 }
2205 }
2206 }
2207
2208 // AuthManager has its own req for some actions
2209 switch ( $providerAction ) {
2210 case self::ACTION_LOGIN:
2211 $reqs[] = new RememberMeAuthenticationRequest( $this->config->get( 'RememberMe' ) );
2212 break;
2213
2215 $reqs[] = new UsernameAuthenticationRequest;
2216 $reqs[] = new UserDataAuthenticationRequest;
2217 if ( $options['username'] !== null ) {
2219 $options['username'] = null; // Don't fill in the username below
2220 }
2221 break;
2222 }
2223
2224 // Fill in reqs data
2225 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2226
2227 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2228 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2229 $reqs = array_filter( $reqs, function ( $req ) {
2230 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2231 } );
2232 }
2233
2234 return array_values( $reqs );
2235 }
2236
2244 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2245 foreach ( $reqs as $req ) {
2246 if ( !$req->action || $forceAction ) {
2247 $req->action = $action;
2248 }
2249 if ( $req->username === null ) {
2250 $req->username = $username;
2251 }
2252 }
2253 }
2254
2261 public function userExists( $username, $flags = User::READ_NORMAL ) {
2262 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2263 if ( $provider->testUserExists( $username, $flags ) ) {
2264 return true;
2265 }
2266 }
2267
2268 return false;
2269 }
2270
2282 public function allowsPropertyChange( $property ) {
2283 $providers = $this->getPrimaryAuthenticationProviders() +
2285 foreach ( $providers as $provider ) {
2286 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2287 return false;
2288 }
2289 }
2290 return true;
2291 }
2292
2301 public function getAuthenticationProvider( $id ) {
2302 // Fast version
2303 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2304 return $this->allAuthenticationProviders[$id];
2305 }
2306
2307 // Slow version: instantiate each kind and check
2308 $providers = $this->getPrimaryAuthenticationProviders();
2309 if ( isset( $providers[$id] ) ) {
2310 return $providers[$id];
2311 }
2312 $providers = $this->getSecondaryAuthenticationProviders();
2313 if ( isset( $providers[$id] ) ) {
2314 return $providers[$id];
2315 }
2316 $providers = $this->getPreAuthenticationProviders();
2317 if ( isset( $providers[$id] ) ) {
2318 return $providers[$id];
2319 }
2320
2321 return null;
2322 }
2323
2324 // endregion -- end of Information methods
2325
2326 /***************************************************************************/
2327 // region Internal methods
2336 public function setAuthenticationSessionData( $key, $data ) {
2337 $session = $this->request->getSession();
2338 $arr = $session->getSecret( 'authData' );
2339 if ( !is_array( $arr ) ) {
2340 $arr = [];
2341 }
2342 $arr[$key] = $data;
2343 $session->setSecret( 'authData', $arr );
2344 }
2345
2353 public function getAuthenticationSessionData( $key, $default = null ) {
2354 $arr = $this->request->getSession()->getSecret( 'authData' );
2355 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2356 return $arr[$key];
2357 } else {
2358 return $default;
2359 }
2360 }
2361
2367 public function removeAuthenticationSessionData( $key ) {
2368 $session = $this->request->getSession();
2369 if ( $key === null ) {
2370 $session->remove( 'authData' );
2371 } else {
2372 $arr = $session->getSecret( 'authData' );
2373 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2374 unset( $arr[$key] );
2375 $session->setSecret( 'authData', $arr );
2376 }
2377 }
2378 }
2379
2386 protected function providerArrayFromSpecs( $class, array $specs ) {
2387 $i = 0;
2388 foreach ( $specs as &$spec ) {
2389 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2390 }
2391 unset( $spec );
2392 // Sort according to the 'sort' field, and if they are equal, according to 'sort2'
2393 usort( $specs, static function ( $a, $b ) {
2394 return $a['sort'] <=> $b['sort']
2395 ?: $a['sort2'] <=> $b['sort2'];
2396 } );
2397
2398 $ret = [];
2399 foreach ( $specs as $spec ) {
2401 $provider = $this->objectFactory->createObject( $spec, [ 'assertClass' => $class ] );
2402 $provider->init( $this->logger, $this, $this->getHookContainer(), $this->config, $this->userNameUtils );
2403 $id = $provider->getUniqueId();
2404 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2405 throw new \RuntimeException(
2406 "Duplicate specifications for id $id (classes " .
2407 get_class( $provider ) . ' and ' .
2408 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2409 );
2410 }
2411 $this->allAuthenticationProviders[$id] = $provider;
2412 $ret[$id] = $provider;
2413 }
2414 return $ret;
2415 }
2416
2420 private function getConfiguration() {
2421 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2422 }
2423
2428 protected function getPreAuthenticationProviders() {
2429 if ( $this->preAuthenticationProviders === null ) {
2430 $conf = $this->getConfiguration();
2431 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2432 PreAuthenticationProvider::class, $conf['preauth']
2433 );
2434 }
2436 }
2437
2443 if ( $this->primaryAuthenticationProviders === null ) {
2444 $conf = $this->getConfiguration();
2445 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2446 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2447 );
2448 }
2450 }
2451
2457 if ( $this->secondaryAuthenticationProviders === null ) {
2458 $conf = $this->getConfiguration();
2459 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2460 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2461 );
2462 }
2464 }
2465
2471 private function setSessionDataForUser( $user, $remember = null ) {
2472 $session = $this->request->getSession();
2473 $delay = $session->delaySave();
2474
2475 $session->resetId();
2476 $session->resetAllTokens();
2477 if ( $session->canSetUser() ) {
2478 $session->setUser( $user );
2479 }
2480 if ( $remember !== null ) {
2481 $session->setRememberUser( $remember );
2482 }
2483 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2484 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2485 $session->persist();
2486
2487 \Wikimedia\ScopedCallback::consume( $delay );
2488
2489 $this->getHookRunner()->onUserLoggedIn( $user );
2490 }
2491
2496 private function setDefaultUserOptions( User $user, $useContextLang ) {
2497 $user->setToken();
2498
2499 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $this->contentLanguage;
2500 $this->userOptionsManager->setOption(
2501 $user,
2502 'language',
2503 $this->languageConverterFactory->getLanguageConverter( $lang )->getPreferredVariant()
2504 );
2505
2506 $contLangConverter = $this->languageConverterFactory->getLanguageConverter( $this->contentLanguage );
2507 if ( $contLangConverter->hasVariants() ) {
2508 $this->userOptionsManager->setOption(
2509 $user,
2510 'variant',
2511 $contLangConverter->getPreferredVariant()
2512 );
2513 }
2514 }
2515
2521 private function callMethodOnProviders( $which, $method, array $args ) {
2522 $providers = [];
2523 if ( $which & 1 ) {
2524 $providers += $this->getPreAuthenticationProviders();
2525 }
2526 if ( $which & 2 ) {
2527 $providers += $this->getPrimaryAuthenticationProviders();
2528 }
2529 if ( $which & 4 ) {
2530 $providers += $this->getSecondaryAuthenticationProviders();
2531 }
2532 foreach ( $providers as $provider ) {
2533 $provider->$method( ...$args );
2534 }
2535 }
2536
2540 private function getHookContainer() {
2541 return $this->hookContainer;
2542 }
2543
2547 private function getHookRunner() {
2548 return $this->hookRunner;
2549 }
2550
2551 // endregion -- end of Internal methods
2552
2553}
2554
2555/*
2556 * This file uses VisualStudio style region/endregion fold markers which are
2557 * recognised by PHPStorm. If modelines are enabled, the following editor
2558 * configuration will also enable folding in vim, if it is in the last 5 lines
2559 * of the file. We also use "@name" which creates sections in Doxygen.
2560 *
2561 * vim: foldmarker=//\ region,//\ endregion foldmethod=marker
2562 */
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:42
A base class that implements some of the boilerplate for a PrimaryAuthenticationProvider.
This serves as the entry point to the authentication system.
canLinkAccounts()
Determine whether accounts can be linked.
const ACTION_UNLINK
Like ACTION_REMOVE but for linking providers only.
const SEC_FAIL
Security-sensitive should not be performed.
UserOptionsManager $userOptionsManager
getPrimaryAuthenticationProviders()
Get the list of PrimaryAuthenticationProviders.
const ACTION_LOGIN_CONTINUE
Continue a login process that was interrupted by the need for user input or communication with an ext...
setAuthenticationSessionData( $key, $data)
Store authentication in the current session.
callMethodOnProviders( $which, $method, array $args)
getAuthenticationProvider( $id)
Get a provider by ID.
setSessionDataForUser( $user, $remember=null)
Log the user in.
securitySensitiveOperationStatus( $operation)
Whether security-sensitive operations should proceed.
SecondaryAuthenticationProvider[] $secondaryAuthenticationProviders
revokeAccessForUser( $username)
Revoke any authentication credentials for a user.
beginAccountLink(User $user, array $reqs, $returnToUrl)
Start an account linking flow.
const SEC_REAUTH
Security-sensitive operations should re-authenticate.
getSecondaryAuthenticationProviders()
Get the list of SecondaryAuthenticationProviders.
allowsPropertyChange( $property)
Determine whether a user property should be allowed to be changed.
const ACTION_CREATE_CONTINUE
Continue a user creation process that was interrupted by the need for user input or communication wit...
getAuthenticationRequestsInternal( $providerAction, array $options, array $providers, UserIdentity $user=null)
Internal request lookup for self::getAuthenticationRequests.
continueAccountLink(array $reqs)
Continue an account linking flow.
UserIdentityLookup $userIdentityLookup
setLogger(LoggerInterface $logger)
allowsAuthenticationDataChange(AuthenticationRequest $req, $checkData=true)
Validate a change of authentication data (e.g.
beginAuthentication(array $reqs, $returnToUrl)
Start an authentication flow.
getAuthenticationSessionData( $key, $default=null)
Fetch authentication data from the current session.
beginAccountCreation(Authority $creator, array $reqs, $returnToUrl)
Start an account creation flow.
canCreateAccounts()
Determine whether accounts can be created.
checkAccountCreatePermissions(Authority $creator)
Basic permissions checks on whether a user can create accounts.
canCreateAccount( $username, $options=[])
Determine whether a particular account can be created.
changeAuthenticationData(AuthenticationRequest $req, $isAddition=false)
Change authentication data (e.g.
userCanAuthenticate( $username)
Determine whether a username can authenticate.
removeAuthenticationSessionData( $key)
Remove authentication data.
providerArrayFromSpecs( $class, array $specs)
Create an array of AuthenticationProviders from an array of ObjectFactory specs.
WatchlistManager $watchlistManager
forcePrimaryAuthenticationProviders(array $providers, $why)
Force certain PrimaryAuthenticationProviders.
const AUTOCREATE_SOURCE_MAINT
Auto-creation is due to a Maintenance script.
setDefaultUserOptions(User $user, $useContextLang)
getAuthenticationRequests( $action, UserIdentity $user=null)
Return the applicable list of AuthenticationRequests.
CreatedAccountAuthenticationRequest[] $createdAccountAuthenticationRequests
AuthenticationProvider[] $allAuthenticationProviders
continueAuthentication(array $reqs)
Continue an authentication flow.
const SEC_OK
Security-sensitive operations are ok.
const ACTION_LINK_CONTINUE
Continue a user linking process that was interrupted by the need for user input or communication with...
const ACTION_CHANGE
Change a user's credentials.
canAuthenticateNow()
Indicate whether user authentication is possible.
const ACTION_REMOVE
Remove a user's credentials.
const ACTION_LINK
Link an existing user to a third-party account.
PreAuthenticationProvider[] $preAuthenticationProviders
__construct(WebRequest $request, Config $config, ObjectFactory $objectFactory, HookContainer $hookContainer, ReadOnlyMode $readOnlyMode, UserNameUtils $userNameUtils, BlockManager $blockManager, WatchlistManager $watchlistManager, ILoadBalancer $loadBalancer, Language $contentLanguage, LanguageConverterFactory $languageConverterFactory, BotPasswordStore $botPasswordStore, UserFactory $userFactory, UserIdentityLookup $userIdentityLookup, UserOptionsManager $userOptionsManager)
BotPasswordStore $botPasswordStore
PrimaryAuthenticationProvider[] $primaryAuthenticationProviders
LanguageConverterFactory $languageConverterFactory
const AUTOCREATE_SOURCE_SESSION
Auto-creation is due to SessionManager.
getPreAuthenticationProviders()
Get the list of PreAuthenticationProviders.
fillRequests(array &$reqs, $action, $username, $forceAction=false)
Set values in an array of requests.
autoCreateUser(User $user, $source, $login=true, $log=true)
Auto-create an account, and optionally log into that account.
userExists( $username, $flags=User::READ_NORMAL)
Determine whether a username exists.
const ACTION_LOGIN
Log in with an existing (not necessarily local) user.
normalizeUsername( $username)
Provide normalized versions of the username for security checks.
continueAccountCreation(array $reqs)
Continue an account creation flow.
const ACTION_CREATE
Create a new user.
This is a value object for authentication requests.
const OPTIONAL
Indicates that the request is not required for authentication to proceed.
const PRIMARY_REQUIRED
Indicates that the request is required by a primary authentication provider.
const REQUIRED
Indicates that the request is required for authentication to proceed.
static getUsernameFromRequests(array $reqs)
Get the username from the set of requests.
static getRequestByClass(array $reqs, $class, $allowSubclasses=false)
Select a request by class name.
const FAIL
Indicates that the authentication failed.
const PASS
Indicates that the authentication succeeded.
const UI
Indicates that the authentication needs further user input of some sort.
const REDIRECT
Indicates that the authentication needs to be redirected to a third party to proceed.
const ABSTAIN
Indicates that the authentication provider does not handle this request.
This transfers state between the login and account creation flows.
Returned from account creation to allow for logging into the created account.
Authentication request for the reason given for account creation.
This is an authentication request added by AuthManager to show a "remember me" checkbox.
const ALWAYS_REMEMBER
Indicates that the user will always be remembered.
const NEVER_REMEMBER
Indicates that the user will never be rememberd.
This represents additional user data requested on the account creation form.
AuthenticationRequest to ensure something with a username is present.
A service class for checking blocks.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
An interface for creating language converters.
A StatusValue for permission errors.
Creates User objects.
UserNameUtils service.
A service class to control user options.
A service class for fetching the wiki's current read-only mode.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2116
addToDatabase()
Add this existing user object to the database.
Definition User.php:3495
loadFromId( $flags=self::READ_NORMAL)
Load user table data, given mId has already been set.
Definition User.php:467
setId( $v)
Set the user and reload all fields according to a given ID.
Definition User.php:2107
getId( $wikiId=self::LOCAL)
Get the user's ID.
Definition User.php:2083
getUserPage()
Get this user's personal page title.
Definition User.php:3655
saveSettings()
Save this user's settings into the database.
Definition User.php:3297
setToken( $token=false)
Set the random token (used for persistent authentication) Called from loadDefaults() among other plac...
Definition User.php:2417
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
Interface for configuration instances.
Definition Config.php:30
An AuthenticationProvider is used by AuthManager when authenticating users.
getUniqueId()
Return a unique identifier for this instance.
A pre-authentication provider can prevent authentication early on.
A primary authentication provider is responsible for associating the submitted authentication data wi...
const TYPE_LINK
Provider can link to existing accounts elsewhere.
const TYPE_NONE
Provider cannot create or link to accounts.
A secondary provider mostly acts when the submitted authentication data has already been associated t...
This interface represents the authority associated the current execution context, such as a web reque...
Definition Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
getUser()
Returns the performer of the actions associated with this authority.
Interface for objects representing user identity.
Database cluster connection, tracking, load balancing, and transaction manager interface.
$cache
Definition mcc.php:33
if( $line===false) $args
Definition mcc.php:124
$source
if(!isset( $args[0])) $lang