Merge "GallerySlideshow: getImageInfo: Reject promise if there is no thumbnail"
[lhc/web/wiklou.git] / includes / auth / AuthManager.php
1 <?php
2 /**
3 * Authentication (and possibly Authorization in the future) system entry point
4 *
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.
9 *
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.
14 *
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
19 *
20 * @file
21 * @ingroup Auth
22 */
23
24 namespace MediaWiki\Auth;
25
26 use Config;
27 use Psr\Log\LoggerAwareInterface;
28 use Psr\Log\LoggerInterface;
29 use Status;
30 use StatusValue;
31 use User;
32 use WebRequest;
33
34 /**
35 * This serves as the entry point to the authentication system.
36 *
37 * In the future, it may also serve as the entry point to the authorization
38 * system.
39 *
40 * @ingroup Auth
41 * @since 1.27
42 */
43 class AuthManager implements LoggerAwareInterface {
44 /** Log in with an existing (not necessarily local) user */
45 const ACTION_LOGIN = 'login';
46 /** Continue a login process that was interrupted by the need for user input or communication
47 * with an external provider */
48 const ACTION_LOGIN_CONTINUE = 'login-continue';
49 /** Create a new user */
50 const ACTION_CREATE = 'create';
51 /** Continue a user creation process that was interrupted by the need for user input or
52 * communication with an external provider */
53 const ACTION_CREATE_CONTINUE = 'create-continue';
54 /** Link an existing user to a third-party account */
55 const ACTION_LINK = 'link';
56 /** Continue a user linking process that was interrupted by the need for user input or
57 * communication with an external provider */
58 const ACTION_LINK_CONTINUE = 'link-continue';
59 /** Change a user's credentials */
60 const ACTION_CHANGE = 'change';
61 /** Remove a user's credentials */
62 const ACTION_REMOVE = 'remove';
63 /** Like ACTION_REMOVE but for linking providers only */
64 const ACTION_UNLINK = 'unlink';
65
66 /** Security-sensitive operations are ok. */
67 const SEC_OK = 'ok';
68 /** Security-sensitive operations should re-authenticate. */
69 const SEC_REAUTH = 'reauth';
70 /** Security-sensitive should not be performed. */
71 const SEC_FAIL = 'fail';
72
73 /** Auto-creation is due to SessionManager */
74 const AUTOCREATE_SOURCE_SESSION = \MediaWiki\Session\SessionManager::class;
75
76 /** @var AuthManager|null */
77 private static $instance = null;
78
79 /** @var WebRequest */
80 private $request;
81
82 /** @var Config */
83 private $config;
84
85 /** @var LoggerInterface */
86 private $logger;
87
88 /** @var AuthenticationProvider[] */
89 private $allAuthenticationProviders = [];
90
91 /** @var PreAuthenticationProvider[] */
92 private $preAuthenticationProviders = null;
93
94 /** @var PrimaryAuthenticationProvider[] */
95 private $primaryAuthenticationProviders = null;
96
97 /** @var SecondaryAuthenticationProvider[] */
98 private $secondaryAuthenticationProviders = null;
99
100 /** @var CreatedAccountAuthenticationRequest[] */
101 private $createdAccountAuthenticationRequests = [];
102
103 /**
104 * Get the global AuthManager
105 * @return AuthManager
106 */
107 public static function singleton() {
108 if ( self::$instance === null ) {
109 self::$instance = new self(
110 \RequestContext::getMain()->getRequest(),
111 \ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
112 );
113 }
114 return self::$instance;
115 }
116
117 /**
118 * @param WebRequest $request
119 * @param Config $config
120 */
121 public function __construct( WebRequest $request, Config $config ) {
122 $this->request = $request;
123 $this->config = $config;
124 $this->setLogger( \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' ) );
125 }
126
127 /**
128 * @param LoggerInterface $logger
129 */
130 public function setLogger( LoggerInterface $logger ) {
131 $this->logger = $logger;
132 }
133
134 /**
135 * @return WebRequest
136 */
137 public function getRequest() {
138 return $this->request;
139 }
140
141 /**
142 * Force certain PrimaryAuthenticationProviders
143 * @deprecated For backwards compatibility only
144 * @param PrimaryAuthenticationProvider[] $providers
145 * @param string $why
146 */
147 public function forcePrimaryAuthenticationProviders( array $providers, $why ) {
148 $this->logger->warning( "Overriding AuthManager primary authn because $why" );
149
150 if ( $this->primaryAuthenticationProviders !== null ) {
151 $this->logger->warning(
152 'PrimaryAuthenticationProviders have already been accessed! I hope nothing breaks.'
153 );
154
155 $this->allAuthenticationProviders = array_diff_key(
156 $this->allAuthenticationProviders,
157 $this->primaryAuthenticationProviders
158 );
159 $session = $this->request->getSession();
160 $session->remove( 'AuthManager::authnState' );
161 $session->remove( 'AuthManager::accountCreationState' );
162 $session->remove( 'AuthManager::accountLinkState' );
163 $this->createdAccountAuthenticationRequests = [];
164 }
165
166 $this->primaryAuthenticationProviders = [];
167 foreach ( $providers as $provider ) {
168 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
169 throw new \RuntimeException(
170 'Expected instance of MediaWiki\\Auth\\PrimaryAuthenticationProvider, got ' .
171 get_class( $provider )
172 );
173 }
174 $provider->setLogger( $this->logger );
175 $provider->setManager( $this );
176 $provider->setConfig( $this->config );
177 $id = $provider->getUniqueId();
178 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
179 throw new \RuntimeException(
180 "Duplicate specifications for id $id (classes " .
181 get_class( $provider ) . ' and ' .
182 get_class( $this->allAuthenticationProviders[$id] ) . ')'
183 );
184 }
185 $this->allAuthenticationProviders[$id] = $provider;
186 $this->primaryAuthenticationProviders[$id] = $provider;
187 }
188 }
189
190 /**
191 * Call a legacy AuthPlugin method, if necessary
192 * @codeCoverageIgnore
193 * @deprecated For backwards compatibility only, should be avoided in new code
194 * @param string $method AuthPlugin method to call
195 * @param array $params Parameters to pass
196 * @param mixed $return Return value if AuthPlugin wasn't called
197 * @return mixed Return value from the AuthPlugin method, or $return
198 */
199 public static function callLegacyAuthPlugin( $method, array $params, $return = null ) {
200 global $wgAuth;
201
202 if ( $wgAuth && !$wgAuth instanceof AuthManagerAuthPlugin ) {
203 return call_user_func_array( [ $wgAuth, $method ], $params );
204 } else {
205 return $return;
206 }
207 }
208
209 /**
210 * @name Authentication
211 * @{
212 */
213
214 /**
215 * Indicate whether user authentication is possible
216 *
217 * It may not be if the session is provided by something like OAuth
218 * for which each individual request includes authentication data.
219 *
220 * @return bool
221 */
222 public function canAuthenticateNow() {
223 return $this->request->getSession()->canSetUser();
224 }
225
226 /**
227 * Start an authentication flow
228 *
229 * In addition to the AuthenticationRequests returned by
230 * $this->getAuthenticationRequests(), a client might include a
231 * CreateFromLoginAuthenticationRequest from a previous login attempt to
232 * preserve state.
233 *
234 * Instead of the AuthenticationRequests returned by
235 * $this->getAuthenticationRequests(), a client might pass a
236 * CreatedAccountAuthenticationRequest from an account creation that just
237 * succeeded to log in to the just-created account.
238 *
239 * @param AuthenticationRequest[] $reqs
240 * @param string $returnToUrl Url that REDIRECT responses should eventually
241 * return to.
242 * @return AuthenticationResponse See self::continueAuthentication()
243 */
244 public function beginAuthentication( array $reqs, $returnToUrl ) {
245 $session = $this->request->getSession();
246 if ( !$session->canSetUser() ) {
247 // Caller should have called canAuthenticateNow()
248 $session->remove( 'AuthManager::authnState' );
249 throw new \LogicException( 'Authentication is not possible now' );
250 }
251
252 $guessUserName = null;
253 foreach ( $reqs as $req ) {
254 $req->returnToUrl = $returnToUrl;
255 // @codeCoverageIgnoreStart
256 if ( $req->username !== null && $req->username !== '' ) {
257 if ( $guessUserName === null ) {
258 $guessUserName = $req->username;
259 } elseif ( $guessUserName !== $req->username ) {
260 $guessUserName = null;
261 break;
262 }
263 }
264 // @codeCoverageIgnoreEnd
265 }
266
267 // Check for special-case login of a just-created account
268 $req = AuthenticationRequest::getRequestByClass(
269 $reqs, CreatedAccountAuthenticationRequest::class
270 );
271 if ( $req ) {
272 if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) {
273 throw new \LogicException(
274 'CreatedAccountAuthenticationRequests are only valid on ' .
275 'the same AuthManager that created the account'
276 );
277 }
278
279 $user = User::newFromName( $req->username );
280 // @codeCoverageIgnoreStart
281 if ( !$user ) {
282 throw new \UnexpectedValueException(
283 "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\""
284 );
285 } elseif ( $user->getId() != $req->id ) {
286 throw new \UnexpectedValueException(
287 "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}"
288 );
289 }
290 // @codeCoverageIgnoreEnd
291
292 $this->logger->info( 'Logging in {user} after account creation', [
293 'user' => $user->getName(),
294 ] );
295 $ret = AuthenticationResponse::newPass( $user->getName() );
296 $this->setSessionDataForUser( $user );
297 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
298 $session->remove( 'AuthManager::authnState' );
299 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
300 return $ret;
301 }
302
303 $this->removeAuthenticationSessionData( null );
304
305 foreach ( $this->getPreAuthenticationProviders() as $provider ) {
306 $status = $provider->testForAuthentication( $reqs );
307 if ( !$status->isGood() ) {
308 $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() );
309 $ret = AuthenticationResponse::newFail(
310 Status::wrap( $status )->getMessage()
311 );
312 $this->callMethodOnProviders( 7, 'postAuthentication',
313 [ User::newFromName( $guessUserName ) ?: null, $ret ]
314 );
315 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] );
316 return $ret;
317 }
318 }
319
320 $state = [
321 'reqs' => $reqs,
322 'returnToUrl' => $returnToUrl,
323 'guessUserName' => $guessUserName,
324 'primary' => null,
325 'primaryResponse' => null,
326 'secondary' => [],
327 'maybeLink' => [],
328 'continueRequests' => [],
329 ];
330
331 // Preserve state from a previous failed login
332 $req = AuthenticationRequest::getRequestByClass(
333 $reqs, CreateFromLoginAuthenticationRequest::class
334 );
335 if ( $req ) {
336 $state['maybeLink'] = $req->maybeLink;
337 }
338
339 $session = $this->request->getSession();
340 $session->setSecret( 'AuthManager::authnState', $state );
341 $session->persist();
342
343 return $this->continueAuthentication( $reqs );
344 }
345
346 /**
347 * Continue an authentication flow
348 *
349 * Return values are interpreted as follows:
350 * - status FAIL: Authentication failed. If $response->createRequest is
351 * set, that may be passed to self::beginAuthentication() or to
352 * self::beginAccountCreation() to preserve state.
353 * - status REDIRECT: The client should be redirected to the contained URL,
354 * new AuthenticationRequests should be made (if any), then
355 * AuthManager::continueAuthentication() should be called.
356 * - status UI: The client should be presented with a user interface for
357 * the fields in the specified AuthenticationRequests, then new
358 * AuthenticationRequests should be made, then
359 * AuthManager::continueAuthentication() should be called.
360 * - status RESTART: The user logged in successfully with a third-party
361 * service, but the third-party credentials aren't attached to any local
362 * account. This could be treated as a UI or a FAIL.
363 * - status PASS: Authentication was successful.
364 *
365 * @param AuthenticationRequest[] $reqs
366 * @return AuthenticationResponse
367 */
368 public function continueAuthentication( array $reqs ) {
369 $session = $this->request->getSession();
370 try {
371 if ( !$session->canSetUser() ) {
372 // Caller should have called canAuthenticateNow()
373 // @codeCoverageIgnoreStart
374 throw new \LogicException( 'Authentication is not possible now' );
375 // @codeCoverageIgnoreEnd
376 }
377
378 $state = $session->getSecret( 'AuthManager::authnState' );
379 if ( !is_array( $state ) ) {
380 return AuthenticationResponse::newFail(
381 wfMessage( 'authmanager-authn-not-in-progress' )
382 );
383 }
384 $state['continueRequests'] = [];
385
386 $guessUserName = $state['guessUserName'];
387
388 foreach ( $reqs as $req ) {
389 $req->returnToUrl = $state['returnToUrl'];
390 }
391
392 // Step 1: Choose an primary authentication provider, and call it until it succeeds.
393
394 if ( $state['primary'] === null ) {
395 // We haven't picked a PrimaryAuthenticationProvider yet
396 // @codeCoverageIgnoreStart
397 $guessUserName = null;
398 foreach ( $reqs as $req ) {
399 if ( $req->username !== null && $req->username !== '' ) {
400 if ( $guessUserName === null ) {
401 $guessUserName = $req->username;
402 } elseif ( $guessUserName !== $req->username ) {
403 $guessUserName = null;
404 break;
405 }
406 }
407 }
408 $state['guessUserName'] = $guessUserName;
409 // @codeCoverageIgnoreEnd
410 $state['reqs'] = $reqs;
411
412 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
413 $res = $provider->beginPrimaryAuthentication( $reqs );
414 switch ( $res->status ) {
415 case AuthenticationResponse::PASS;
416 $state['primary'] = $id;
417 $state['primaryResponse'] = $res;
418 $this->logger->debug( "Primary login with $id succeeded" );
419 break 2;
420 case AuthenticationResponse::FAIL;
421 $this->logger->debug( "Login failed in primary authentication by $id" );
422 if ( $res->createRequest || $state['maybeLink'] ) {
423 $res->createRequest = new CreateFromLoginAuthenticationRequest(
424 $res->createRequest, $state['maybeLink']
425 );
426 }
427 $this->callMethodOnProviders( 7, 'postAuthentication',
428 [ User::newFromName( $guessUserName ) ?: null, $res ]
429 );
430 $session->remove( 'AuthManager::authnState' );
431 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
432 return $res;
433 case AuthenticationResponse::ABSTAIN;
434 // Continue loop
435 break;
436 case AuthenticationResponse::REDIRECT;
437 case AuthenticationResponse::UI;
438 $this->logger->debug( "Primary login with $id returned $res->status" );
439 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
440 $state['primary'] = $id;
441 $state['continueRequests'] = $res->neededRequests;
442 $session->setSecret( 'AuthManager::authnState', $state );
443 return $res;
444
445 // @codeCoverageIgnoreStart
446 default:
447 throw new \DomainException(
448 get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status"
449 );
450 // @codeCoverageIgnoreEnd
451 }
452 }
453 if ( $state['primary'] === null ) {
454 $this->logger->debug( 'Login failed in primary authentication because no provider accepted' );
455 $ret = AuthenticationResponse::newFail(
456 wfMessage( 'authmanager-authn-no-primary' )
457 );
458 $this->callMethodOnProviders( 7, 'postAuthentication',
459 [ User::newFromName( $guessUserName ) ?: null, $ret ]
460 );
461 $session->remove( 'AuthManager::authnState' );
462 return $ret;
463 }
464 } elseif ( $state['primaryResponse'] === null ) {
465 $provider = $this->getAuthenticationProvider( $state['primary'] );
466 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
467 // Configuration changed? Force them to start over.
468 // @codeCoverageIgnoreStart
469 $ret = AuthenticationResponse::newFail(
470 wfMessage( 'authmanager-authn-not-in-progress' )
471 );
472 $this->callMethodOnProviders( 7, 'postAuthentication',
473 [ User::newFromName( $guessUserName ) ?: null, $ret ]
474 );
475 $session->remove( 'AuthManager::authnState' );
476 return $ret;
477 // @codeCoverageIgnoreEnd
478 }
479 $id = $provider->getUniqueId();
480 $res = $provider->continuePrimaryAuthentication( $reqs );
481 switch ( $res->status ) {
482 case AuthenticationResponse::PASS;
483 $state['primaryResponse'] = $res;
484 $this->logger->debug( "Primary login with $id succeeded" );
485 break;
486 case AuthenticationResponse::FAIL;
487 $this->logger->debug( "Login failed in primary authentication by $id" );
488 if ( $res->createRequest || $state['maybeLink'] ) {
489 $res->createRequest = new CreateFromLoginAuthenticationRequest(
490 $res->createRequest, $state['maybeLink']
491 );
492 }
493 $this->callMethodOnProviders( 7, 'postAuthentication',
494 [ User::newFromName( $guessUserName ) ?: null, $res ]
495 );
496 $session->remove( 'AuthManager::authnState' );
497 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] );
498 return $res;
499 case AuthenticationResponse::REDIRECT;
500 case AuthenticationResponse::UI;
501 $this->logger->debug( "Primary login with $id returned $res->status" );
502 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName );
503 $state['continueRequests'] = $res->neededRequests;
504 $session->setSecret( 'AuthManager::authnState', $state );
505 return $res;
506 default:
507 throw new \DomainException(
508 get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status"
509 );
510 }
511 }
512
513 $res = $state['primaryResponse'];
514 if ( $res->username === null ) {
515 $provider = $this->getAuthenticationProvider( $state['primary'] );
516 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
517 // Configuration changed? Force them to start over.
518 // @codeCoverageIgnoreStart
519 $ret = AuthenticationResponse::newFail(
520 wfMessage( 'authmanager-authn-not-in-progress' )
521 );
522 $this->callMethodOnProviders( 7, 'postAuthentication',
523 [ User::newFromName( $guessUserName ) ?: null, $ret ]
524 );
525 $session->remove( 'AuthManager::authnState' );
526 return $ret;
527 // @codeCoverageIgnoreEnd
528 }
529
530 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK &&
531 $res->linkRequest &&
532 // don't confuse the user with an incorrect message if linking is disabled
533 $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class )
534 ) {
535 $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest;
536 $msg = 'authmanager-authn-no-local-user-link';
537 } else {
538 $msg = 'authmanager-authn-no-local-user';
539 }
540 $this->logger->debug(
541 "Primary login with {$provider->getUniqueId()} succeeded, but returned no user"
542 );
543 $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) );
544 $ret->neededRequests = $this->getAuthenticationRequestsInternal(
545 self::ACTION_LOGIN,
546 [],
547 $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()
548 );
549 if ( $res->createRequest || $state['maybeLink'] ) {
550 $ret->createRequest = new CreateFromLoginAuthenticationRequest(
551 $res->createRequest, $state['maybeLink']
552 );
553 $ret->neededRequests[] = $ret->createRequest;
554 }
555 $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true );
556 $session->setSecret( 'AuthManager::authnState', [
557 'reqs' => [], // Will be filled in later
558 'primary' => null,
559 'primaryResponse' => null,
560 'secondary' => [],
561 'continueRequests' => $ret->neededRequests,
562 ] + $state );
563 return $ret;
564 }
565
566 // Step 2: Primary authentication succeeded, create the User object
567 // (and add the user locally if necessary)
568
569 $user = User::newFromName( $res->username, 'usable' );
570 if ( !$user ) {
571 throw new \DomainException(
572 get_class( $provider ) . " returned an invalid username: {$res->username}"
573 );
574 }
575 if ( $user->getId() === 0 ) {
576 // User doesn't exist locally. Create it.
577 $this->logger->info( 'Auto-creating {user} on login', [
578 'user' => $user->getName(),
579 ] );
580 $status = $this->autoCreateUser( $user, $state['primary'], false );
581 if ( !$status->isGood() ) {
582 $ret = AuthenticationResponse::newFail(
583 Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' )
584 );
585 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
586 $session->remove( 'AuthManager::authnState' );
587 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
588 return $ret;
589 }
590 }
591
592 // Step 3: Iterate over all the secondary authentication providers.
593
594 $beginReqs = $state['reqs'];
595
596 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
597 if ( !isset( $state['secondary'][$id] ) ) {
598 // This provider isn't started yet, so we pass it the set
599 // of reqs from beginAuthentication instead of whatever
600 // might have been used by a previous provider in line.
601 $func = 'beginSecondaryAuthentication';
602 $res = $provider->beginSecondaryAuthentication( $user, $beginReqs );
603 } elseif ( !$state['secondary'][$id] ) {
604 $func = 'continueSecondaryAuthentication';
605 $res = $provider->continueSecondaryAuthentication( $user, $reqs );
606 } else {
607 continue;
608 }
609 switch ( $res->status ) {
610 case AuthenticationResponse::PASS;
611 $this->logger->debug( "Secondary login with $id succeeded" );
612 // fall through
613 case AuthenticationResponse::ABSTAIN;
614 $state['secondary'][$id] = true;
615 break;
616 case AuthenticationResponse::FAIL;
617 $this->logger->debug( "Login failed in secondary authentication by $id" );
618 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] );
619 $session->remove( 'AuthManager::authnState' );
620 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] );
621 return $res;
622 case AuthenticationResponse::REDIRECT;
623 case AuthenticationResponse::UI;
624 $this->logger->debug( "Secondary login with $id returned " . $res->status );
625 $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() );
626 $state['secondary'][$id] = false;
627 $state['continueRequests'] = $res->neededRequests;
628 $session->setSecret( 'AuthManager::authnState', $state );
629 return $res;
630
631 // @codeCoverageIgnoreStart
632 default:
633 throw new \DomainException(
634 get_class( $provider ) . "::{$func}() returned $res->status"
635 );
636 // @codeCoverageIgnoreEnd
637 }
638 }
639
640 // Step 4: Authentication complete! Set the user in the session and
641 // clean up.
642
643 $this->logger->info( 'Login for {user} succeeded', [
644 'user' => $user->getName(),
645 ] );
646 $req = AuthenticationRequest::getRequestByClass(
647 $beginReqs, RememberMeAuthenticationRequest::class
648 );
649 $this->setSessionDataForUser( $user, $req && $req->rememberMe );
650 $ret = AuthenticationResponse::newPass( $user->getName() );
651 $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] );
652 $session->remove( 'AuthManager::authnState' );
653 $this->removeAuthenticationSessionData( null );
654 \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] );
655 return $ret;
656 } catch ( \Exception $ex ) {
657 $session->remove( 'AuthManager::authnState' );
658 throw $ex;
659 }
660 }
661
662 /**
663 * Whether security-sensitive operations should proceed.
664 *
665 * A "security-sensitive operation" is something like a password or email
666 * change, that would normally have a "reenter your password to confirm"
667 * box if we only supported password-based authentication.
668 *
669 * @param string $operation Operation being checked. This should be a
670 * message-key-like string such as 'change-password' or 'change-email'.
671 * @return string One of the SEC_* constants.
672 */
673 public function securitySensitiveOperationStatus( $operation ) {
674 $status = self::SEC_OK;
675
676 $this->logger->debug( __METHOD__ . ": Checking $operation" );
677
678 $session = $this->request->getSession();
679 $aId = $session->getUser()->getId();
680 if ( $aId === 0 ) {
681 // User isn't authenticated. DWIM?
682 $status = $this->canAuthenticateNow() ? self::SEC_REAUTH : self::SEC_FAIL;
683 $this->logger->info( __METHOD__ . ": Not logged in! $operation is $status" );
684 return $status;
685 }
686
687 if ( $session->canSetUser() ) {
688 $id = $session->get( 'AuthManager:lastAuthId' );
689 $last = $session->get( 'AuthManager:lastAuthTimestamp' );
690 if ( $id !== $aId || $last === null ) {
691 $timeSinceLogin = PHP_INT_MAX; // Forever ago
692 } else {
693 $timeSinceLogin = max( 0, time() - $last );
694 }
695
696 $thresholds = $this->config->get( 'ReauthenticateTime' );
697 if ( isset( $thresholds[$operation] ) ) {
698 $threshold = $thresholds[$operation];
699 } elseif ( isset( $thresholds['default'] ) ) {
700 $threshold = $thresholds['default'];
701 } else {
702 throw new \UnexpectedValueException( '$wgReauthenticateTime lacks a default' );
703 }
704
705 if ( $threshold >= 0 && $timeSinceLogin > $threshold ) {
706 $status = self::SEC_REAUTH;
707 }
708 } else {
709 $timeSinceLogin = -1;
710
711 $pass = $this->config->get( 'AllowSecuritySensitiveOperationIfCannotReauthenticate' );
712 if ( isset( $pass[$operation] ) ) {
713 $status = $pass[$operation] ? self::SEC_OK : self::SEC_FAIL;
714 } elseif ( isset( $pass['default'] ) ) {
715 $status = $pass['default'] ? self::SEC_OK : self::SEC_FAIL;
716 } else {
717 throw new \UnexpectedValueException(
718 '$wgAllowSecuritySensitiveOperationIfCannotReauthenticate lacks a default'
719 );
720 }
721 }
722
723 \Hooks::run( 'SecuritySensitiveOperationStatus', [
724 &$status, $operation, $session, $timeSinceLogin
725 ] );
726
727 // If authentication is not possible, downgrade from "REAUTH" to "FAIL".
728 if ( !$this->canAuthenticateNow() && $status === self::SEC_REAUTH ) {
729 $status = self::SEC_FAIL;
730 }
731
732 $this->logger->info( __METHOD__ . ": $operation is $status" );
733
734 return $status;
735 }
736
737 /**
738 * Determine whether a username can authenticate
739 *
740 * @param string $username
741 * @return bool
742 */
743 public function userCanAuthenticate( $username ) {
744 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
745 if ( $provider->testUserCanAuthenticate( $username ) ) {
746 return true;
747 }
748 }
749 return false;
750 }
751
752 /**
753 * Provide normalized versions of the username for security checks
754 *
755 * Since different providers can normalize the input in different ways,
756 * this returns an array of all the different ways the name might be
757 * normalized for authentication.
758 *
759 * The returned strings should not be revealed to the user, as that might
760 * leak private information (e.g. an email address might be normalized to a
761 * username).
762 *
763 * @param string $username
764 * @return string[]
765 */
766 public function normalizeUsername( $username ) {
767 $ret = [];
768 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
769 $normalized = $provider->providerNormalizeUsername( $username );
770 if ( $normalized !== null ) {
771 $ret[$normalized] = true;
772 }
773 }
774 return array_keys( $ret );
775 }
776
777 /**@}*/
778
779 /**
780 * @name Authentication data changing
781 * @{
782 */
783
784 /**
785 * Revoke any authentication credentials for a user
786 *
787 * After this, the user should no longer be able to log in.
788 *
789 * @param string $username
790 */
791 public function revokeAccessForUser( $username ) {
792 $this->logger->info( 'Revoking access for {user}', [
793 'user' => $username,
794 ] );
795 $this->callMethodOnProviders( 6, 'providerRevokeAccessForUser', [ $username ] );
796 }
797
798 /**
799 * Validate a change of authentication data (e.g. passwords)
800 * @param AuthenticationRequest $req
801 * @param bool $checkData If false, $req hasn't been loaded from the
802 * submission so checks on user-submitted fields should be skipped. $req->username is
803 * considered user-submitted for this purpose, even if it cannot be changed via
804 * $req->loadFromSubmission.
805 * @return Status
806 */
807 public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) {
808 $any = false;
809 $providers = $this->getPrimaryAuthenticationProviders() +
810 $this->getSecondaryAuthenticationProviders();
811 foreach ( $providers as $provider ) {
812 $status = $provider->providerAllowsAuthenticationDataChange( $req, $checkData );
813 if ( !$status->isGood() ) {
814 return Status::wrap( $status );
815 }
816 $any = $any || $status->value !== 'ignored';
817 }
818 if ( !$any ) {
819 $status = Status::newGood( 'ignored' );
820 $status->warning( 'authmanager-change-not-supported' );
821 return $status;
822 }
823 return Status::newGood();
824 }
825
826 /**
827 * Change authentication data (e.g. passwords)
828 *
829 * If $req was returned for AuthManager::ACTION_CHANGE, using $req should
830 * result in a successful login in the future.
831 *
832 * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
833 * no longer result in a successful login.
834 *
835 * @param AuthenticationRequest $req
836 */
837 public function changeAuthenticationData( AuthenticationRequest $req ) {
838 $this->logger->info( 'Changing authentication data for {user} class {what}', [
839 'user' => is_string( $req->username ) ? $req->username : '<no name>',
840 'what' => get_class( $req ),
841 ] );
842
843 $this->callMethodOnProviders( 6, 'providerChangeAuthenticationData', [ $req ] );
844
845 // When the main account's authentication data is changed, invalidate
846 // all BotPasswords too.
847 \BotPassword::invalidateAllPasswordsForUser( $req->username );
848 }
849
850 /**@}*/
851
852 /**
853 * @name Account creation
854 * @{
855 */
856
857 /**
858 * Determine whether accounts can be created
859 * @return bool
860 */
861 public function canCreateAccounts() {
862 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
863 switch ( $provider->accountCreationType() ) {
864 case PrimaryAuthenticationProvider::TYPE_CREATE:
865 case PrimaryAuthenticationProvider::TYPE_LINK:
866 return true;
867 }
868 }
869 return false;
870 }
871
872 /**
873 * Determine whether a particular account can be created
874 * @param string $username
875 * @param array $options
876 * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
877 * - creating: (bool) For internal use only. Never specify this.
878 * @return Status
879 */
880 public function canCreateAccount( $username, $options = [] ) {
881 // Back compat
882 if ( is_int( $options ) ) {
883 $options = [ 'flags' => $options ];
884 }
885 $options += [
886 'flags' => User::READ_NORMAL,
887 'creating' => false,
888 ];
889 $flags = $options['flags'];
890
891 if ( !$this->canCreateAccounts() ) {
892 return Status::newFatal( 'authmanager-create-disabled' );
893 }
894
895 if ( $this->userExists( $username, $flags ) ) {
896 return Status::newFatal( 'userexists' );
897 }
898
899 $user = User::newFromName( $username, 'creatable' );
900 if ( !is_object( $user ) ) {
901 return Status::newFatal( 'noname' );
902 } else {
903 $user->load( $flags ); // Explicitly load with $flags, auto-loading always uses READ_NORMAL
904 if ( $user->getId() !== 0 ) {
905 return Status::newFatal( 'userexists' );
906 }
907 }
908
909 // Denied by providers?
910 $providers = $this->getPreAuthenticationProviders() +
911 $this->getPrimaryAuthenticationProviders() +
912 $this->getSecondaryAuthenticationProviders();
913 foreach ( $providers as $provider ) {
914 $status = $provider->testUserForCreation( $user, false, $options );
915 if ( !$status->isGood() ) {
916 return Status::wrap( $status );
917 }
918 }
919
920 return Status::newGood();
921 }
922
923 /**
924 * Basic permissions checks on whether a user can create accounts
925 * @param User $creator User doing the account creation
926 * @return Status
927 */
928 public function checkAccountCreatePermissions( User $creator ) {
929 // Wiki is read-only?
930 if ( wfReadOnly() ) {
931 return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
932 }
933
934 // This is awful, this permission check really shouldn't go through Title.
935 $permErrors = \SpecialPage::getTitleFor( 'CreateAccount' )
936 ->getUserPermissionsErrors( 'createaccount', $creator, 'secure' );
937 if ( $permErrors ) {
938 $status = Status::newGood();
939 foreach ( $permErrors as $args ) {
940 call_user_func_array( [ $status, 'fatal' ], $args );
941 }
942 return $status;
943 }
944
945 $block = $creator->isBlockedFromCreateAccount();
946 if ( $block ) {
947 $errorParams = [
948 $block->getTarget(),
949 $block->mReason ?: wfMessage( 'blockednoreason' )->text(),
950 $block->getByName()
951 ];
952
953 if ( $block->getType() === \Block::TYPE_RANGE ) {
954 $errorMessage = 'cantcreateaccount-range-text';
955 $errorParams[] = $this->getRequest()->getIP();
956 } else {
957 $errorMessage = 'cantcreateaccount-text';
958 }
959
960 return Status::newFatal( wfMessage( $errorMessage, $errorParams ) );
961 }
962
963 $ip = $this->getRequest()->getIP();
964 if ( $creator->isDnsBlacklisted( $ip, true /* check $wgProxyWhitelist */ ) ) {
965 return Status::newFatal( 'sorbs_create_account_reason' );
966 }
967
968 return Status::newGood();
969 }
970
971 /**
972 * Start an account creation flow
973 *
974 * In addition to the AuthenticationRequests returned by
975 * $this->getAuthenticationRequests(), a client might include a
976 * CreateFromLoginAuthenticationRequest from a previous login attempt. If
977 * <code>
978 * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE )
979 * </code>
980 * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests
981 * should be omitted. If the CreateFromLoginAuthenticationRequest has a
982 * username set, that username must be used for all other requests.
983 *
984 * @param User $creator User doing the account creation
985 * @param AuthenticationRequest[] $reqs
986 * @param string $returnToUrl Url that REDIRECT responses should eventually
987 * return to.
988 * @return AuthenticationResponse
989 */
990 public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) {
991 $session = $this->request->getSession();
992 if ( !$this->canCreateAccounts() ) {
993 // Caller should have called canCreateAccounts()
994 $session->remove( 'AuthManager::accountCreationState' );
995 throw new \LogicException( 'Account creation is not possible' );
996 }
997
998 try {
999 $username = AuthenticationRequest::getUsernameFromRequests( $reqs );
1000 } catch ( \UnexpectedValueException $ex ) {
1001 $username = null;
1002 }
1003 if ( $username === null ) {
1004 $this->logger->debug( __METHOD__ . ': No username provided' );
1005 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1006 }
1007
1008 // Permissions check
1009 $status = $this->checkAccountCreatePermissions( $creator );
1010 if ( !$status->isGood() ) {
1011 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1012 'user' => $username,
1013 'creator' => $creator->getName(),
1014 'reason' => $status->getWikiText( null, null, 'en' )
1015 ] );
1016 return AuthenticationResponse::newFail( $status->getMessage() );
1017 }
1018
1019 $status = $this->canCreateAccount(
1020 $username, [ 'flags' => User::READ_LOCKING, 'creating' => true ]
1021 );
1022 if ( !$status->isGood() ) {
1023 $this->logger->debug( __METHOD__ . ': {user} cannot be created: {reason}', [
1024 'user' => $username,
1025 'creator' => $creator->getName(),
1026 'reason' => $status->getWikiText( null, null, 'en' )
1027 ] );
1028 return AuthenticationResponse::newFail( $status->getMessage() );
1029 }
1030
1031 $user = User::newFromName( $username, 'creatable' );
1032 foreach ( $reqs as $req ) {
1033 $req->username = $username;
1034 $req->returnToUrl = $returnToUrl;
1035 if ( $req instanceof UserDataAuthenticationRequest ) {
1036 $status = $req->populateUser( $user );
1037 if ( !$status->isGood() ) {
1038 $status = Status::wrap( $status );
1039 $session->remove( 'AuthManager::accountCreationState' );
1040 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1041 'user' => $user->getName(),
1042 'creator' => $creator->getName(),
1043 'reason' => $status->getWikiText( null, null, 'en' ),
1044 ] );
1045 return AuthenticationResponse::newFail( $status->getMessage() );
1046 }
1047 }
1048 }
1049
1050 $this->removeAuthenticationSessionData( null );
1051
1052 $state = [
1053 'username' => $username,
1054 'userid' => 0,
1055 'creatorid' => $creator->getId(),
1056 'creatorname' => $creator->getName(),
1057 'reqs' => $reqs,
1058 'returnToUrl' => $returnToUrl,
1059 'primary' => null,
1060 'primaryResponse' => null,
1061 'secondary' => [],
1062 'continueRequests' => [],
1063 'maybeLink' => [],
1064 'ranPreTests' => false,
1065 ];
1066
1067 // Special case: converting a login to an account creation
1068 $req = AuthenticationRequest::getRequestByClass(
1069 $reqs, CreateFromLoginAuthenticationRequest::class
1070 );
1071 if ( $req ) {
1072 $state['maybeLink'] = $req->maybeLink;
1073
1074 if ( $req->createRequest ) {
1075 $reqs[] = $req->createRequest;
1076 $state['reqs'][] = $req->createRequest;
1077 }
1078 }
1079
1080 $session->setSecret( 'AuthManager::accountCreationState', $state );
1081 $session->persist();
1082
1083 return $this->continueAccountCreation( $reqs );
1084 }
1085
1086 /**
1087 * Continue an account creation flow
1088 * @param AuthenticationRequest[] $reqs
1089 * @return AuthenticationResponse
1090 */
1091 public function continueAccountCreation( array $reqs ) {
1092 $session = $this->request->getSession();
1093 try {
1094 if ( !$this->canCreateAccounts() ) {
1095 // Caller should have called canCreateAccounts()
1096 $session->remove( 'AuthManager::accountCreationState' );
1097 throw new \LogicException( 'Account creation is not possible' );
1098 }
1099
1100 $state = $session->getSecret( 'AuthManager::accountCreationState' );
1101 if ( !is_array( $state ) ) {
1102 return AuthenticationResponse::newFail(
1103 wfMessage( 'authmanager-create-not-in-progress' )
1104 );
1105 }
1106 $state['continueRequests'] = [];
1107
1108 // Step 0: Prepare and validate the input
1109
1110 $user = User::newFromName( $state['username'], 'creatable' );
1111 if ( !is_object( $user ) ) {
1112 $session->remove( 'AuthManager::accountCreationState' );
1113 $this->logger->debug( __METHOD__ . ': Invalid username', [
1114 'user' => $state['username'],
1115 ] );
1116 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1117 }
1118
1119 if ( $state['creatorid'] ) {
1120 $creator = User::newFromId( $state['creatorid'] );
1121 } else {
1122 $creator = new User;
1123 $creator->setName( $state['creatorname'] );
1124 }
1125
1126 // Avoid account creation races on double submissions
1127 $cache = \ObjectCache::getLocalClusterInstance();
1128 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $user->getName() ) ) );
1129 if ( !$lock ) {
1130 // Don't clear AuthManager::accountCreationState for this code
1131 // path because the process that won the race owns it.
1132 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1133 'user' => $user->getName(),
1134 'creator' => $creator->getName(),
1135 ] );
1136 return AuthenticationResponse::newFail( wfMessage( 'usernameinprogress' ) );
1137 }
1138
1139 // Permissions check
1140 $status = $this->checkAccountCreatePermissions( $creator );
1141 if ( !$status->isGood() ) {
1142 $this->logger->debug( __METHOD__ . ': {creator} cannot create users: {reason}', [
1143 'user' => $user->getName(),
1144 'creator' => $creator->getName(),
1145 'reason' => $status->getWikiText( null, null, 'en' )
1146 ] );
1147 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1148 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1149 $session->remove( 'AuthManager::accountCreationState' );
1150 return $ret;
1151 }
1152
1153 // Load from master for existence check
1154 $user->load( User::READ_LOCKING );
1155
1156 if ( $state['userid'] === 0 ) {
1157 if ( $user->getId() != 0 ) {
1158 $this->logger->debug( __METHOD__ . ': User exists locally', [
1159 'user' => $user->getName(),
1160 'creator' => $creator->getName(),
1161 ] );
1162 $ret = AuthenticationResponse::newFail( wfMessage( 'userexists' ) );
1163 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1164 $session->remove( 'AuthManager::accountCreationState' );
1165 return $ret;
1166 }
1167 } else {
1168 if ( $user->getId() == 0 ) {
1169 $this->logger->debug( __METHOD__ . ': User does not exist locally when it should', [
1170 'user' => $user->getName(),
1171 'creator' => $creator->getName(),
1172 'expected_id' => $state['userid'],
1173 ] );
1174 throw new \UnexpectedValueException(
1175 "User \"{$state['username']}\" should exist now, but doesn't!"
1176 );
1177 }
1178 if ( $user->getId() != $state['userid'] ) {
1179 $this->logger->debug( __METHOD__ . ': User ID/name mismatch', [
1180 'user' => $user->getName(),
1181 'creator' => $creator->getName(),
1182 'expected_id' => $state['userid'],
1183 'actual_id' => $user->getId(),
1184 ] );
1185 throw new \UnexpectedValueException(
1186 "User \"{$state['username']}\" exists, but " .
1187 "ID {$user->getId()} != {$state['userid']}!"
1188 );
1189 }
1190 }
1191 foreach ( $state['reqs'] as $req ) {
1192 if ( $req instanceof UserDataAuthenticationRequest ) {
1193 $status = $req->populateUser( $user );
1194 if ( !$status->isGood() ) {
1195 // This should never happen...
1196 $status = Status::wrap( $status );
1197 $this->logger->debug( __METHOD__ . ': UserData is invalid: {reason}', [
1198 'user' => $user->getName(),
1199 'creator' => $creator->getName(),
1200 'reason' => $status->getWikiText( null, null, 'en' ),
1201 ] );
1202 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1203 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1204 $session->remove( 'AuthManager::accountCreationState' );
1205 return $ret;
1206 }
1207 }
1208 }
1209
1210 foreach ( $reqs as $req ) {
1211 $req->returnToUrl = $state['returnToUrl'];
1212 $req->username = $state['username'];
1213 }
1214
1215 // Run pre-creation tests, if we haven't already
1216 if ( !$state['ranPreTests'] ) {
1217 $providers = $this->getPreAuthenticationProviders() +
1218 $this->getPrimaryAuthenticationProviders() +
1219 $this->getSecondaryAuthenticationProviders();
1220 foreach ( $providers as $id => $provider ) {
1221 $status = $provider->testForAccountCreation( $user, $creator, $reqs );
1222 if ( !$status->isGood() ) {
1223 $this->logger->debug( __METHOD__ . ": Fail in pre-authentication by $id", [
1224 'user' => $user->getName(),
1225 'creator' => $creator->getName(),
1226 ] );
1227 $ret = AuthenticationResponse::newFail(
1228 Status::wrap( $status )->getMessage()
1229 );
1230 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1231 $session->remove( 'AuthManager::accountCreationState' );
1232 return $ret;
1233 }
1234 }
1235
1236 $state['ranPreTests'] = true;
1237 }
1238
1239 // Step 1: Choose a primary authentication provider and call it until it succeeds.
1240
1241 if ( $state['primary'] === null ) {
1242 // We haven't picked a PrimaryAuthenticationProvider yet
1243 foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) {
1244 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE ) {
1245 continue;
1246 }
1247 $res = $provider->beginPrimaryAccountCreation( $user, $creator, $reqs );
1248 switch ( $res->status ) {
1249 case AuthenticationResponse::PASS;
1250 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1251 'user' => $user->getName(),
1252 'creator' => $creator->getName(),
1253 ] );
1254 $state['primary'] = $id;
1255 $state['primaryResponse'] = $res;
1256 break 2;
1257 case AuthenticationResponse::FAIL;
1258 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1259 'user' => $user->getName(),
1260 'creator' => $creator->getName(),
1261 ] );
1262 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1263 $session->remove( 'AuthManager::accountCreationState' );
1264 return $res;
1265 case AuthenticationResponse::ABSTAIN;
1266 // Continue loop
1267 break;
1268 case AuthenticationResponse::REDIRECT;
1269 case AuthenticationResponse::UI;
1270 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1271 'user' => $user->getName(),
1272 'creator' => $creator->getName(),
1273 ] );
1274 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1275 $state['primary'] = $id;
1276 $state['continueRequests'] = $res->neededRequests;
1277 $session->setSecret( 'AuthManager::accountCreationState', $state );
1278 return $res;
1279
1280 // @codeCoverageIgnoreStart
1281 default:
1282 throw new \DomainException(
1283 get_class( $provider ) . "::beginPrimaryAccountCreation() returned $res->status"
1284 );
1285 // @codeCoverageIgnoreEnd
1286 }
1287 }
1288 if ( $state['primary'] === null ) {
1289 $this->logger->debug( __METHOD__ . ': Primary creation failed because no provider accepted', [
1290 'user' => $user->getName(),
1291 'creator' => $creator->getName(),
1292 ] );
1293 $ret = AuthenticationResponse::newFail(
1294 wfMessage( 'authmanager-create-no-primary' )
1295 );
1296 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1297 $session->remove( 'AuthManager::accountCreationState' );
1298 return $ret;
1299 }
1300 } elseif ( $state['primaryResponse'] === null ) {
1301 $provider = $this->getAuthenticationProvider( $state['primary'] );
1302 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1303 // Configuration changed? Force them to start over.
1304 // @codeCoverageIgnoreStart
1305 $ret = AuthenticationResponse::newFail(
1306 wfMessage( 'authmanager-create-not-in-progress' )
1307 );
1308 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1309 $session->remove( 'AuthManager::accountCreationState' );
1310 return $ret;
1311 // @codeCoverageIgnoreEnd
1312 }
1313 $id = $provider->getUniqueId();
1314 $res = $provider->continuePrimaryAccountCreation( $user, $creator, $reqs );
1315 switch ( $res->status ) {
1316 case AuthenticationResponse::PASS;
1317 $this->logger->debug( __METHOD__ . ": Primary creation passed by $id", [
1318 'user' => $user->getName(),
1319 'creator' => $creator->getName(),
1320 ] );
1321 $state['primaryResponse'] = $res;
1322 break;
1323 case AuthenticationResponse::FAIL;
1324 $this->logger->debug( __METHOD__ . ": Primary creation failed by $id", [
1325 'user' => $user->getName(),
1326 'creator' => $creator->getName(),
1327 ] );
1328 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $res ] );
1329 $session->remove( 'AuthManager::accountCreationState' );
1330 return $res;
1331 case AuthenticationResponse::REDIRECT;
1332 case AuthenticationResponse::UI;
1333 $this->logger->debug( __METHOD__ . ": Primary creation $res->status by $id", [
1334 'user' => $user->getName(),
1335 'creator' => $creator->getName(),
1336 ] );
1337 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1338 $state['continueRequests'] = $res->neededRequests;
1339 $session->setSecret( 'AuthManager::accountCreationState', $state );
1340 return $res;
1341 default:
1342 throw new \DomainException(
1343 get_class( $provider ) . "::continuePrimaryAccountCreation() returned $res->status"
1344 );
1345 }
1346 }
1347
1348 // Step 2: Primary authentication succeeded, create the User object
1349 // and add the user locally.
1350
1351 if ( $state['userid'] === 0 ) {
1352 $this->logger->info( 'Creating user {user} during account creation', [
1353 'user' => $user->getName(),
1354 'creator' => $creator->getName(),
1355 ] );
1356 $status = $user->addToDatabase();
1357 if ( !$status->isOk() ) {
1358 // @codeCoverageIgnoreStart
1359 $ret = AuthenticationResponse::newFail( $status->getMessage() );
1360 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1361 $session->remove( 'AuthManager::accountCreationState' );
1362 return $ret;
1363 // @codeCoverageIgnoreEnd
1364 }
1365 $this->setDefaultUserOptions( $user, $creator->isAnon() );
1366 \Hooks::run( 'LocalUserCreated', [ $user, false ] );
1367 $user->saveSettings();
1368 $state['userid'] = $user->getId();
1369
1370 // Update user count
1371 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1372
1373 // Watch user's userpage and talk page
1374 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1375
1376 // Inform the provider
1377 $logSubtype = $provider->finishAccountCreation( $user, $creator, $state['primaryResponse'] );
1378
1379 // Log the creation
1380 if ( $this->config->get( 'NewUserLog' ) ) {
1381 $isAnon = $creator->isAnon();
1382 $logEntry = new \ManualLogEntry(
1383 'newusers',
1384 $logSubtype ?: ( $isAnon ? 'create' : 'create2' )
1385 );
1386 $logEntry->setPerformer( $isAnon ? $user : $creator );
1387 $logEntry->setTarget( $user->getUserPage() );
1388 $req = AuthenticationRequest::getRequestByClass(
1389 $state['reqs'], CreationReasonAuthenticationRequest::class
1390 );
1391 $logEntry->setComment( $req ? $req->reason : '' );
1392 $logEntry->setParameters( [
1393 '4::userid' => $user->getId(),
1394 ] );
1395 $logid = $logEntry->insert();
1396 $logEntry->publish( $logid );
1397 }
1398 }
1399
1400 // Step 3: Iterate over all the secondary authentication providers.
1401
1402 $beginReqs = $state['reqs'];
1403
1404 foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) {
1405 if ( !isset( $state['secondary'][$id] ) ) {
1406 // This provider isn't started yet, so we pass it the set
1407 // of reqs from beginAuthentication instead of whatever
1408 // might have been used by a previous provider in line.
1409 $func = 'beginSecondaryAccountCreation';
1410 $res = $provider->beginSecondaryAccountCreation( $user, $creator, $beginReqs );
1411 } elseif ( !$state['secondary'][$id] ) {
1412 $func = 'continueSecondaryAccountCreation';
1413 $res = $provider->continueSecondaryAccountCreation( $user, $creator, $reqs );
1414 } else {
1415 continue;
1416 }
1417 switch ( $res->status ) {
1418 case AuthenticationResponse::PASS;
1419 $this->logger->debug( __METHOD__ . ": Secondary creation passed by $id", [
1420 'user' => $user->getName(),
1421 'creator' => $creator->getName(),
1422 ] );
1423 // fall through
1424 case AuthenticationResponse::ABSTAIN;
1425 $state['secondary'][$id] = true;
1426 break;
1427 case AuthenticationResponse::REDIRECT;
1428 case AuthenticationResponse::UI;
1429 $this->logger->debug( __METHOD__ . ": Secondary creation $res->status by $id", [
1430 'user' => $user->getName(),
1431 'creator' => $creator->getName(),
1432 ] );
1433 $this->fillRequests( $res->neededRequests, self::ACTION_CREATE, null );
1434 $state['secondary'][$id] = false;
1435 $state['continueRequests'] = $res->neededRequests;
1436 $session->setSecret( 'AuthManager::accountCreationState', $state );
1437 return $res;
1438 case AuthenticationResponse::FAIL;
1439 throw new \DomainException(
1440 get_class( $provider ) . "::{$func}() returned $res->status." .
1441 ' Secondary providers are not allowed to fail account creation, that' .
1442 ' should have been done via testForAccountCreation().'
1443 );
1444 // @codeCoverageIgnoreStart
1445 default:
1446 throw new \DomainException(
1447 get_class( $provider ) . "::{$func}() returned $res->status"
1448 );
1449 // @codeCoverageIgnoreEnd
1450 }
1451 }
1452
1453 $id = $user->getId();
1454 $name = $user->getName();
1455 $req = new CreatedAccountAuthenticationRequest( $id, $name );
1456 $ret = AuthenticationResponse::newPass( $name );
1457 $ret->loginRequest = $req;
1458 $this->createdAccountAuthenticationRequests[] = $req;
1459
1460 $this->logger->info( __METHOD__ . ': Account creation succeeded for {user}', [
1461 'user' => $user->getName(),
1462 'creator' => $creator->getName(),
1463 ] );
1464
1465 $this->callMethodOnProviders( 7, 'postAccountCreation', [ $user, $creator, $ret ] );
1466 $session->remove( 'AuthManager::accountCreationState' );
1467 $this->removeAuthenticationSessionData( null );
1468 return $ret;
1469 } catch ( \Exception $ex ) {
1470 $session->remove( 'AuthManager::accountCreationState' );
1471 throw $ex;
1472 }
1473 }
1474
1475 /**
1476 * Auto-create an account, and log into that account
1477 * @param User $user User to auto-create
1478 * @param string $source What caused the auto-creation? This must be the ID
1479 * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
1480 * @param bool $login Whether to also log the user in
1481 * @return Status Good if user was created, Ok if user already existed, otherwise Fatal
1482 */
1483 public function autoCreateUser( User $user, $source, $login = true ) {
1484 if ( $source !== self::AUTOCREATE_SOURCE_SESSION &&
1485 !$this->getAuthenticationProvider( $source ) instanceof PrimaryAuthenticationProvider
1486 ) {
1487 throw new \InvalidArgumentException( "Unknown auto-creation source: $source" );
1488 }
1489
1490 $username = $user->getName();
1491
1492 // Try the local user from the slave DB
1493 $localId = User::idFromName( $username );
1494 $flags = User::READ_NORMAL;
1495
1496 // Fetch the user ID from the master, so that we don't try to create the user
1497 // when they already exist, due to replication lag
1498 // @codeCoverageIgnoreStart
1499 if ( !$localId && wfGetLB()->getReaderIndex() != 0 ) {
1500 $localId = User::idFromName( $username, User::READ_LATEST );
1501 $flags = User::READ_LATEST;
1502 }
1503 // @codeCoverageIgnoreEnd
1504
1505 if ( $localId ) {
1506 $this->logger->debug( __METHOD__ . ': {username} already exists locally', [
1507 'username' => $username,
1508 ] );
1509 $user->setId( $localId );
1510 $user->loadFromId( $flags );
1511 if ( $login ) {
1512 $this->setSessionDataForUser( $user );
1513 }
1514 $status = Status::newGood();
1515 $status->warning( 'userexists' );
1516 return $status;
1517 }
1518
1519 // Wiki is read-only?
1520 if ( wfReadOnly() ) {
1521 $this->logger->debug( __METHOD__ . ': denied by wfReadOnly(): {reason}', [
1522 'username' => $username,
1523 'reason' => wfReadOnlyReason(),
1524 ] );
1525 $user->setId( 0 );
1526 $user->loadFromId();
1527 return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
1528 }
1529
1530 // Check the session, if we tried to create this user already there's
1531 // no point in retrying.
1532 $session = $this->request->getSession();
1533 if ( $session->get( 'AuthManager::AutoCreateBlacklist' ) ) {
1534 $this->logger->debug( __METHOD__ . ': blacklisted in session {sessionid}', [
1535 'username' => $username,
1536 'sessionid' => $session->getId(),
1537 ] );
1538 $user->setId( 0 );
1539 $user->loadFromId();
1540 $reason = $session->get( 'AuthManager::AutoCreateBlacklist' );
1541 if ( $reason instanceof StatusValue ) {
1542 return Status::wrap( $reason );
1543 } else {
1544 return Status::newFatal( $reason );
1545 }
1546 }
1547
1548 // Is the username creatable?
1549 if ( !User::isCreatableName( $username ) ) {
1550 $this->logger->debug( __METHOD__ . ': name "{username}" is not creatable', [
1551 'username' => $username,
1552 ] );
1553 $session->set( 'AuthManager::AutoCreateBlacklist', 'noname', 600 );
1554 $user->setId( 0 );
1555 $user->loadFromId();
1556 return Status::newFatal( 'noname' );
1557 }
1558
1559 // Is the IP user able to create accounts?
1560 $anon = new User;
1561 if ( !$anon->isAllowedAny( 'createaccount', 'autocreateaccount' ) ) {
1562 $this->logger->debug( __METHOD__ . ': IP lacks the ability to create or autocreate accounts', [
1563 'username' => $username,
1564 'ip' => $anon->getName(),
1565 ] );
1566 $session->set( 'AuthManager::AutoCreateBlacklist', 'authmanager-autocreate-noperm', 600 );
1567 $session->persist();
1568 $user->setId( 0 );
1569 $user->loadFromId();
1570 return Status::newFatal( 'authmanager-autocreate-noperm' );
1571 }
1572
1573 // Avoid account creation races on double submissions
1574 $cache = \ObjectCache::getLocalClusterInstance();
1575 $lock = $cache->getScopedLock( $cache->makeGlobalKey( 'account', md5( $username ) ) );
1576 if ( !$lock ) {
1577 $this->logger->debug( __METHOD__ . ': Could not acquire account creation lock', [
1578 'user' => $username,
1579 ] );
1580 $user->setId( 0 );
1581 $user->loadFromId();
1582 return Status::newFatal( 'usernameinprogress' );
1583 }
1584
1585 // Denied by providers?
1586 $options = [
1587 'flags' => User::READ_LATEST,
1588 'creating' => true,
1589 ];
1590 $providers = $this->getPreAuthenticationProviders() +
1591 $this->getPrimaryAuthenticationProviders() +
1592 $this->getSecondaryAuthenticationProviders();
1593 foreach ( $providers as $provider ) {
1594 $status = $provider->testUserForCreation( $user, $source, $options );
1595 if ( !$status->isGood() ) {
1596 $ret = Status::wrap( $status );
1597 $this->logger->debug( __METHOD__ . ': Provider denied creation of {username}: {reason}', [
1598 'username' => $username,
1599 'reason' => $ret->getWikiText( null, null, 'en' ),
1600 ] );
1601 $session->set( 'AuthManager::AutoCreateBlacklist', $status, 600 );
1602 $user->setId( 0 );
1603 $user->loadFromId();
1604 return $ret;
1605 }
1606 }
1607
1608 $backoffKey = wfMemcKey( 'AuthManager', 'autocreate-failed', md5( $username ) );
1609 if ( $cache->get( $backoffKey ) ) {
1610 $this->logger->debug( __METHOD__ . ': {username} denied by prior creation attempt failures', [
1611 'username' => $username,
1612 ] );
1613 $user->setId( 0 );
1614 $user->loadFromId();
1615 return Status::newFatal( 'authmanager-autocreate-exception' );
1616 }
1617
1618 // Checks passed, create the user...
1619 $from = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : 'CLI';
1620 $this->logger->info( __METHOD__ . ': creating new user ({username}) - from: {from}', [
1621 'username' => $username,
1622 'from' => $from,
1623 ] );
1624
1625 // Ignore warnings about master connections/writes...hard to avoid here
1626 $trxProfiler = \Profiler::instance()->getTransactionProfiler();
1627 $trxProfiler->setSilenced( true );
1628 try {
1629 $status = $user->addToDatabase();
1630 if ( !$status->isOk() ) {
1631 // double-check for a race condition (T70012)
1632 $localId = User::idFromName( $username, User::READ_LATEST );
1633 if ( $localId ) {
1634 $this->logger->info( __METHOD__ . ': {username} already exists locally (race)', [
1635 'username' => $username,
1636 ] );
1637 $user->setId( $localId );
1638 $user->loadFromId( User::READ_LATEST );
1639 if ( $login ) {
1640 $this->setSessionDataForUser( $user );
1641 }
1642 $status = Status::newGood();
1643 $status->warning( 'userexists' );
1644 } else {
1645 $this->logger->error( __METHOD__ . ': {username} failed with message {message}', [
1646 'username' => $username,
1647 'message' => $status->getWikiText( null, null, 'en' )
1648 ] );
1649 $user->setId( 0 );
1650 $user->loadFromId();
1651 }
1652 return $status;
1653 }
1654 } catch ( \Exception $ex ) {
1655 $trxProfiler->setSilenced( false );
1656 $this->logger->error( __METHOD__ . ': {username} failed with exception {exception}', [
1657 'username' => $username,
1658 'exception' => $ex,
1659 ] );
1660 // Do not keep throwing errors for a while
1661 $cache->set( $backoffKey, 1, 600 );
1662 // Bubble up error; which should normally trigger DB rollbacks
1663 throw $ex;
1664 }
1665
1666 $this->setDefaultUserOptions( $user, false );
1667
1668 // Inform the providers
1669 $this->callMethodOnProviders( 6, 'autoCreatedAccount', [ $user, $source ] );
1670
1671 \Hooks::run( 'AuthPluginAutoCreate', [ $user ], '1.27' );
1672 \Hooks::run( 'LocalUserCreated', [ $user, true ] );
1673 $user->saveSettings();
1674
1675 // Update user count
1676 \DeferredUpdates::addUpdate( new \SiteStatsUpdate( 0, 0, 0, 0, 1 ) );
1677 // Watch user's userpage and talk page
1678 \DeferredUpdates::addCallableUpdate( function () use ( $user ) {
1679 $user->addWatch( $user->getUserPage(), User::IGNORE_USER_RIGHTS );
1680 } );
1681
1682 // Log the creation
1683 if ( $this->config->get( 'NewUserLog' ) ) {
1684 $logEntry = new \ManualLogEntry( 'newusers', 'autocreate' );
1685 $logEntry->setPerformer( $user );
1686 $logEntry->setTarget( $user->getUserPage() );
1687 $logEntry->setComment( '' );
1688 $logEntry->setParameters( [
1689 '4::userid' => $user->getId(),
1690 ] );
1691 $logid = $logEntry->insert();
1692 }
1693
1694 // Commit database changes, so even if something else later blows up
1695 // the newly-created user doesn't get lost.
1696 wfGetLBFactory()->commitMasterChanges( __METHOD__ );
1697
1698 $trxProfiler->setSilenced( false );
1699
1700 if ( $login ) {
1701 $this->setSessionDataForUser( $user );
1702 }
1703
1704 return Status::newGood();
1705 }
1706
1707 /**@}*/
1708
1709 /**
1710 * @name Account linking
1711 * @{
1712 */
1713
1714 /**
1715 * Determine whether accounts can be linked
1716 * @return bool
1717 */
1718 public function canLinkAccounts() {
1719 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
1720 if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK ) {
1721 return true;
1722 }
1723 }
1724 return false;
1725 }
1726
1727 /**
1728 * Start an account linking flow
1729 *
1730 * @param User $user User being linked
1731 * @param AuthenticationRequest[] $reqs
1732 * @param string $returnToUrl Url that REDIRECT responses should eventually
1733 * return to.
1734 * @return AuthenticationResponse
1735 */
1736 public function beginAccountLink( User $user, array $reqs, $returnToUrl ) {
1737 $session = $this->request->getSession();
1738 $session->remove( 'AuthManager::accountLinkState' );
1739
1740 if ( !$this->canLinkAccounts() ) {
1741 // Caller should have called canLinkAccounts()
1742 throw new \LogicException( 'Account linking is not possible' );
1743 }
1744
1745 if ( $user->getId() === 0 ) {
1746 if ( !User::isUsableName( $user->getName() ) ) {
1747 $msg = wfMessage( 'noname' );
1748 } else {
1749 $msg = wfMessage( 'authmanager-userdoesnotexist', $user->getName() );
1750 }
1751 return AuthenticationResponse::newFail( $msg );
1752 }
1753 foreach ( $reqs as $req ) {
1754 $req->username = $user->getName();
1755 $req->returnToUrl = $returnToUrl;
1756 }
1757
1758 $this->removeAuthenticationSessionData( null );
1759
1760 $providers = $this->getPreAuthenticationProviders();
1761 foreach ( $providers as $id => $provider ) {
1762 $status = $provider->testForAccountLink( $user );
1763 if ( !$status->isGood() ) {
1764 $this->logger->debug( __METHOD__ . ": Account linking pre-check failed by $id", [
1765 'user' => $user->getName(),
1766 ] );
1767 $ret = AuthenticationResponse::newFail(
1768 Status::wrap( $status )->getMessage()
1769 );
1770 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1771 return $ret;
1772 }
1773 }
1774
1775 $state = [
1776 'username' => $user->getName(),
1777 'userid' => $user->getId(),
1778 'returnToUrl' => $returnToUrl,
1779 'primary' => null,
1780 'continueRequests' => [],
1781 ];
1782
1783 $providers = $this->getPrimaryAuthenticationProviders();
1784 foreach ( $providers as $id => $provider ) {
1785 if ( $provider->accountCreationType() !== PrimaryAuthenticationProvider::TYPE_LINK ) {
1786 continue;
1787 }
1788
1789 $res = $provider->beginPrimaryAccountLink( $user, $reqs );
1790 switch ( $res->status ) {
1791 case AuthenticationResponse::PASS;
1792 $this->logger->info( "Account linked to {user} by $id", [
1793 'user' => $user->getName(),
1794 ] );
1795 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1796 return $res;
1797
1798 case AuthenticationResponse::FAIL;
1799 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1800 'user' => $user->getName(),
1801 ] );
1802 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1803 return $res;
1804
1805 case AuthenticationResponse::ABSTAIN;
1806 // Continue loop
1807 break;
1808
1809 case AuthenticationResponse::REDIRECT;
1810 case AuthenticationResponse::UI;
1811 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1812 'user' => $user->getName(),
1813 ] );
1814 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1815 $state['primary'] = $id;
1816 $state['continueRequests'] = $res->neededRequests;
1817 $session->setSecret( 'AuthManager::accountLinkState', $state );
1818 $session->persist();
1819 return $res;
1820
1821 // @codeCoverageIgnoreStart
1822 default:
1823 throw new \DomainException(
1824 get_class( $provider ) . "::beginPrimaryAccountLink() returned $res->status"
1825 );
1826 // @codeCoverageIgnoreEnd
1827 }
1828 }
1829
1830 $this->logger->debug( __METHOD__ . ': Account linking failed because no provider accepted', [
1831 'user' => $user->getName(),
1832 ] );
1833 $ret = AuthenticationResponse::newFail(
1834 wfMessage( 'authmanager-link-no-primary' )
1835 );
1836 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1837 return $ret;
1838 }
1839
1840 /**
1841 * Continue an account linking flow
1842 * @param AuthenticationRequest[] $reqs
1843 * @return AuthenticationResponse
1844 */
1845 public function continueAccountLink( array $reqs ) {
1846 $session = $this->request->getSession();
1847 try {
1848 if ( !$this->canLinkAccounts() ) {
1849 // Caller should have called canLinkAccounts()
1850 $session->remove( 'AuthManager::accountLinkState' );
1851 throw new \LogicException( 'Account linking is not possible' );
1852 }
1853
1854 $state = $session->getSecret( 'AuthManager::accountLinkState' );
1855 if ( !is_array( $state ) ) {
1856 return AuthenticationResponse::newFail(
1857 wfMessage( 'authmanager-link-not-in-progress' )
1858 );
1859 }
1860 $state['continueRequests'] = [];
1861
1862 // Step 0: Prepare and validate the input
1863
1864 $user = User::newFromName( $state['username'], 'usable' );
1865 if ( !is_object( $user ) ) {
1866 $session->remove( 'AuthManager::accountLinkState' );
1867 return AuthenticationResponse::newFail( wfMessage( 'noname' ) );
1868 }
1869 if ( $user->getId() != $state['userid'] ) {
1870 throw new \UnexpectedValueException(
1871 "User \"{$state['username']}\" is valid, but " .
1872 "ID {$user->getId()} != {$state['userid']}!"
1873 );
1874 }
1875
1876 foreach ( $reqs as $req ) {
1877 $req->username = $state['username'];
1878 $req->returnToUrl = $state['returnToUrl'];
1879 }
1880
1881 // Step 1: Call the primary again until it succeeds
1882
1883 $provider = $this->getAuthenticationProvider( $state['primary'] );
1884 if ( !$provider instanceof PrimaryAuthenticationProvider ) {
1885 // Configuration changed? Force them to start over.
1886 // @codeCoverageIgnoreStart
1887 $ret = AuthenticationResponse::newFail(
1888 wfMessage( 'authmanager-link-not-in-progress' )
1889 );
1890 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $ret ] );
1891 $session->remove( 'AuthManager::accountLinkState' );
1892 return $ret;
1893 // @codeCoverageIgnoreEnd
1894 }
1895 $id = $provider->getUniqueId();
1896 $res = $provider->continuePrimaryAccountLink( $user, $reqs );
1897 switch ( $res->status ) {
1898 case AuthenticationResponse::PASS;
1899 $this->logger->info( "Account linked to {user} by $id", [
1900 'user' => $user->getName(),
1901 ] );
1902 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1903 $session->remove( 'AuthManager::accountLinkState' );
1904 return $res;
1905 case AuthenticationResponse::FAIL;
1906 $this->logger->debug( __METHOD__ . ": Account linking failed by $id", [
1907 'user' => $user->getName(),
1908 ] );
1909 $this->callMethodOnProviders( 3, 'postAccountLink', [ $user, $res ] );
1910 $session->remove( 'AuthManager::accountLinkState' );
1911 return $res;
1912 case AuthenticationResponse::REDIRECT;
1913 case AuthenticationResponse::UI;
1914 $this->logger->debug( __METHOD__ . ": Account linking $res->status by $id", [
1915 'user' => $user->getName(),
1916 ] );
1917 $this->fillRequests( $res->neededRequests, self::ACTION_LINK, $user->getName() );
1918 $state['continueRequests'] = $res->neededRequests;
1919 $session->setSecret( 'AuthManager::accountLinkState', $state );
1920 return $res;
1921 default:
1922 throw new \DomainException(
1923 get_class( $provider ) . "::continuePrimaryAccountLink() returned $res->status"
1924 );
1925 }
1926 } catch ( \Exception $ex ) {
1927 $session->remove( 'AuthManager::accountLinkState' );
1928 throw $ex;
1929 }
1930 }
1931
1932 /**@}*/
1933
1934 /**
1935 * @name Information methods
1936 * @{
1937 */
1938
1939 /**
1940 * Return the applicable list of AuthenticationRequests
1941 *
1942 * Possible values for $action:
1943 * - ACTION_LOGIN: Valid for passing to beginAuthentication
1944 * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state
1945 * - ACTION_CREATE: Valid for passing to beginAccountCreation
1946 * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state
1947 * - ACTION_LINK: Valid for passing to beginAccountLink
1948 * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state
1949 * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials
1950 * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials.
1951 * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts.
1952 *
1953 * @param string $action One of the AuthManager::ACTION_* constants
1954 * @param User|null $user User being acted on, instead of the current user.
1955 * @return AuthenticationRequest[]
1956 */
1957 public function getAuthenticationRequests( $action, User $user = null ) {
1958 $options = [];
1959 $providerAction = $action;
1960
1961 // Figure out which providers to query
1962 switch ( $action ) {
1963 case self::ACTION_LOGIN:
1964 case self::ACTION_CREATE:
1965 $providers = $this->getPreAuthenticationProviders() +
1966 $this->getPrimaryAuthenticationProviders() +
1967 $this->getSecondaryAuthenticationProviders();
1968 break;
1969
1970 case self::ACTION_LOGIN_CONTINUE:
1971 $state = $this->request->getSession()->getSecret( 'AuthManager::authnState' );
1972 return is_array( $state ) ? $state['continueRequests'] : [];
1973
1974 case self::ACTION_CREATE_CONTINUE:
1975 $state = $this->request->getSession()->getSecret( 'AuthManager::accountCreationState' );
1976 return is_array( $state ) ? $state['continueRequests'] : [];
1977
1978 case self::ACTION_LINK:
1979 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1980 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1981 } );
1982 break;
1983
1984 case self::ACTION_UNLINK:
1985 $providers = array_filter( $this->getPrimaryAuthenticationProviders(), function ( $p ) {
1986 return $p->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK;
1987 } );
1988
1989 // To providers, unlink and remove are identical.
1990 $providerAction = self::ACTION_REMOVE;
1991 break;
1992
1993 case self::ACTION_LINK_CONTINUE:
1994 $state = $this->request->getSession()->getSecret( 'AuthManager::accountLinkState' );
1995 return is_array( $state ) ? $state['continueRequests'] : [];
1996
1997 case self::ACTION_CHANGE:
1998 case self::ACTION_REMOVE:
1999 $providers = $this->getPrimaryAuthenticationProviders() +
2000 $this->getSecondaryAuthenticationProviders();
2001 break;
2002
2003 // @codeCoverageIgnoreStart
2004 default:
2005 throw new \DomainException( __METHOD__ . ": Invalid action \"$action\"" );
2006 }
2007 // @codeCoverageIgnoreEnd
2008
2009 return $this->getAuthenticationRequestsInternal( $providerAction, $options, $providers, $user );
2010 }
2011
2012 /**
2013 * Internal request lookup for self::getAuthenticationRequests
2014 *
2015 * @param string $providerAction Action to pass to providers
2016 * @param array $options Options to pass to providers
2017 * @param AuthenticationProvider[] $providers
2018 * @param User|null $user
2019 * @return AuthenticationRequest[]
2020 */
2021 private function getAuthenticationRequestsInternal(
2022 $providerAction, array $options, array $providers, User $user = null
2023 ) {
2024 $user = $user ?: \RequestContext::getMain()->getUser();
2025 $options['username'] = $user->isAnon() ? null : $user->getName();
2026
2027 // Query them and merge results
2028 $reqs = [];
2029 $allPrimaryRequired = null;
2030 foreach ( $providers as $provider ) {
2031 $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
2032 $thisRequired = [];
2033 foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
2034 $id = $req->getUniqueId();
2035
2036 // If it's from a Primary, mark it as "primary-required" but
2037 // track it for later.
2038 if ( $isPrimary ) {
2039 if ( $req->required ) {
2040 $thisRequired[$id] = true;
2041 $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
2042 }
2043 }
2044
2045 if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
2046 $reqs[$id] = $req;
2047 }
2048 }
2049
2050 // Track which requests are required by all primaries
2051 if ( $isPrimary ) {
2052 $allPrimaryRequired = $allPrimaryRequired === null
2053 ? $thisRequired
2054 : array_intersect_key( $allPrimaryRequired, $thisRequired );
2055 }
2056 }
2057 // Any requests that were required by all primaries are required.
2058 foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
2059 $reqs[$id]->required = AuthenticationRequest::REQUIRED;
2060 }
2061
2062 // AuthManager has its own req for some actions
2063 switch ( $providerAction ) {
2064 case self::ACTION_LOGIN:
2065 $reqs[] = new RememberMeAuthenticationRequest;
2066 break;
2067
2068 case self::ACTION_CREATE:
2069 $reqs[] = new UsernameAuthenticationRequest;
2070 $reqs[] = new UserDataAuthenticationRequest;
2071 if ( $options['username'] !== null ) {
2072 $reqs[] = new CreationReasonAuthenticationRequest;
2073 $options['username'] = null; // Don't fill in the username below
2074 }
2075 break;
2076 }
2077
2078 // Fill in reqs data
2079 $this->fillRequests( $reqs, $providerAction, $options['username'], true );
2080
2081 // For self::ACTION_CHANGE, filter out any that something else *doesn't* allow changing
2082 if ( $providerAction === self::ACTION_CHANGE || $providerAction === self::ACTION_REMOVE ) {
2083 $reqs = array_filter( $reqs, function ( $req ) {
2084 return $this->allowsAuthenticationDataChange( $req, false )->isGood();
2085 } );
2086 }
2087
2088 return array_values( $reqs );
2089 }
2090
2091 /**
2092 * Set values in an array of requests
2093 * @param AuthenticationRequest[] &$reqs
2094 * @param string $action
2095 * @param string|null $username
2096 * @param boolean $forceAction
2097 */
2098 private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) {
2099 foreach ( $reqs as $req ) {
2100 if ( !$req->action || $forceAction ) {
2101 $req->action = $action;
2102 }
2103 if ( $req->username === null ) {
2104 $req->username = $username;
2105 }
2106 }
2107 }
2108
2109 /**
2110 * Determine whether a username exists
2111 * @param string $username
2112 * @param int $flags Bitfield of User:READ_* constants
2113 * @return bool
2114 */
2115 public function userExists( $username, $flags = User::READ_NORMAL ) {
2116 foreach ( $this->getPrimaryAuthenticationProviders() as $provider ) {
2117 if ( $provider->testUserExists( $username, $flags ) ) {
2118 return true;
2119 }
2120 }
2121
2122 return false;
2123 }
2124
2125 /**
2126 * Determine whether a user property should be allowed to be changed.
2127 *
2128 * Supported properties are:
2129 * - emailaddress
2130 * - realname
2131 * - nickname
2132 *
2133 * @param string $property
2134 * @return bool
2135 */
2136 public function allowsPropertyChange( $property ) {
2137 $providers = $this->getPrimaryAuthenticationProviders() +
2138 $this->getSecondaryAuthenticationProviders();
2139 foreach ( $providers as $provider ) {
2140 if ( !$provider->providerAllowsPropertyChange( $property ) ) {
2141 return false;
2142 }
2143 }
2144 return true;
2145 }
2146
2147 /**
2148 * Get a provider by ID
2149 * @note This is public so extensions can check whether their own provider
2150 * is installed and so they can read its configuration if necessary.
2151 * Other uses are not recommended.
2152 * @param string $id
2153 * @return AuthenticationProvider|null
2154 */
2155 public function getAuthenticationProvider( $id ) {
2156 // Fast version
2157 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2158 return $this->allAuthenticationProviders[$id];
2159 }
2160
2161 // Slow version: instantiate each kind and check
2162 $providers = $this->getPrimaryAuthenticationProviders();
2163 if ( isset( $providers[$id] ) ) {
2164 return $providers[$id];
2165 }
2166 $providers = $this->getSecondaryAuthenticationProviders();
2167 if ( isset( $providers[$id] ) ) {
2168 return $providers[$id];
2169 }
2170 $providers = $this->getPreAuthenticationProviders();
2171 if ( isset( $providers[$id] ) ) {
2172 return $providers[$id];
2173 }
2174
2175 return null;
2176 }
2177
2178 /**@}*/
2179
2180 /**
2181 * @name Internal methods
2182 * @{
2183 */
2184
2185 /**
2186 * Store authentication in the current session
2187 * @protected For use by AuthenticationProviders
2188 * @param string $key
2189 * @param mixed $data Must be serializable
2190 */
2191 public function setAuthenticationSessionData( $key, $data ) {
2192 $session = $this->request->getSession();
2193 $arr = $session->getSecret( 'authData' );
2194 if ( !is_array( $arr ) ) {
2195 $arr = [];
2196 }
2197 $arr[$key] = $data;
2198 $session->setSecret( 'authData', $arr );
2199 }
2200
2201 /**
2202 * Fetch authentication data from the current session
2203 * @protected For use by AuthenticationProviders
2204 * @param string $key
2205 * @param mixed $default
2206 * @return mixed
2207 */
2208 public function getAuthenticationSessionData( $key, $default = null ) {
2209 $arr = $this->request->getSession()->getSecret( 'authData' );
2210 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2211 return $arr[$key];
2212 } else {
2213 return $default;
2214 }
2215 }
2216
2217 /**
2218 * Remove authentication data
2219 * @protected For use by AuthenticationProviders
2220 * @param string|null $key If null, all data is removed
2221 */
2222 public function removeAuthenticationSessionData( $key ) {
2223 $session = $this->request->getSession();
2224 if ( $key === null ) {
2225 $session->remove( 'authData' );
2226 } else {
2227 $arr = $session->getSecret( 'authData' );
2228 if ( is_array( $arr ) && array_key_exists( $key, $arr ) ) {
2229 unset( $arr[$key] );
2230 $session->setSecret( 'authData', $arr );
2231 }
2232 }
2233 }
2234
2235 /**
2236 * Create an array of AuthenticationProviders from an array of ObjectFactory specs
2237 * @param string $class
2238 * @param array[] $specs
2239 * @return AuthenticationProvider[]
2240 */
2241 protected function providerArrayFromSpecs( $class, array $specs ) {
2242 $i = 0;
2243 foreach ( $specs as &$spec ) {
2244 $spec = [ 'sort2' => $i++ ] + $spec + [ 'sort' => 0 ];
2245 }
2246 unset( $spec );
2247 usort( $specs, function ( $a, $b ) {
2248 return ( (int)$a['sort'] ) - ( (int)$b['sort'] )
2249 ?: $a['sort2'] - $b['sort2'];
2250 } );
2251
2252 $ret = [];
2253 foreach ( $specs as $spec ) {
2254 $provider = \ObjectFactory::getObjectFromSpec( $spec );
2255 if ( !$provider instanceof $class ) {
2256 throw new \RuntimeException(
2257 "Expected instance of $class, got " . get_class( $provider )
2258 );
2259 }
2260 $provider->setLogger( $this->logger );
2261 $provider->setManager( $this );
2262 $provider->setConfig( $this->config );
2263 $id = $provider->getUniqueId();
2264 if ( isset( $this->allAuthenticationProviders[$id] ) ) {
2265 throw new \RuntimeException(
2266 "Duplicate specifications for id $id (classes " .
2267 get_class( $provider ) . ' and ' .
2268 get_class( $this->allAuthenticationProviders[$id] ) . ')'
2269 );
2270 }
2271 $this->allAuthenticationProviders[$id] = $provider;
2272 $ret[$id] = $provider;
2273 }
2274 return $ret;
2275 }
2276
2277 /**
2278 * Get the configuration
2279 * @return array
2280 */
2281 private function getConfiguration() {
2282 return $this->config->get( 'AuthManagerConfig' ) ?: $this->config->get( 'AuthManagerAutoConfig' );
2283 }
2284
2285 /**
2286 * Get the list of PreAuthenticationProviders
2287 * @return PreAuthenticationProvider[]
2288 */
2289 protected function getPreAuthenticationProviders() {
2290 if ( $this->preAuthenticationProviders === null ) {
2291 $conf = $this->getConfiguration();
2292 $this->preAuthenticationProviders = $this->providerArrayFromSpecs(
2293 PreAuthenticationProvider::class, $conf['preauth']
2294 );
2295 }
2296 return $this->preAuthenticationProviders;
2297 }
2298
2299 /**
2300 * Get the list of PrimaryAuthenticationProviders
2301 * @return PrimaryAuthenticationProvider[]
2302 */
2303 protected function getPrimaryAuthenticationProviders() {
2304 if ( $this->primaryAuthenticationProviders === null ) {
2305 $conf = $this->getConfiguration();
2306 $this->primaryAuthenticationProviders = $this->providerArrayFromSpecs(
2307 PrimaryAuthenticationProvider::class, $conf['primaryauth']
2308 );
2309 }
2310 return $this->primaryAuthenticationProviders;
2311 }
2312
2313 /**
2314 * Get the list of SecondaryAuthenticationProviders
2315 * @return SecondaryAuthenticationProvider[]
2316 */
2317 protected function getSecondaryAuthenticationProviders() {
2318 if ( $this->secondaryAuthenticationProviders === null ) {
2319 $conf = $this->getConfiguration();
2320 $this->secondaryAuthenticationProviders = $this->providerArrayFromSpecs(
2321 SecondaryAuthenticationProvider::class, $conf['secondaryauth']
2322 );
2323 }
2324 return $this->secondaryAuthenticationProviders;
2325 }
2326
2327 /**
2328 * @param User $user
2329 * @param bool|null $remember
2330 */
2331 private function setSessionDataForUser( $user, $remember = null ) {
2332 $session = $this->request->getSession();
2333 $delay = $session->delaySave();
2334
2335 $session->resetId();
2336 $session->resetAllTokens();
2337 if ( $session->canSetUser() ) {
2338 $session->setUser( $user );
2339 }
2340 if ( $remember !== null ) {
2341 $session->setRememberUser( $remember );
2342 }
2343 $session->set( 'AuthManager:lastAuthId', $user->getId() );
2344 $session->set( 'AuthManager:lastAuthTimestamp', time() );
2345 $session->persist();
2346
2347 \ScopedCallback::consume( $delay );
2348
2349 \Hooks::run( 'UserLoggedIn', [ $user ] );
2350 }
2351
2352 /**
2353 * @param User $user
2354 * @param bool $useContextLang Use 'uselang' to set the user's language
2355 */
2356 private function setDefaultUserOptions( User $user, $useContextLang ) {
2357 global $wgContLang;
2358
2359 $user->setToken();
2360
2361 $lang = $useContextLang ? \RequestContext::getMain()->getLanguage() : $wgContLang;
2362 $user->setOption( 'language', $lang->getPreferredVariant() );
2363
2364 if ( $wgContLang->hasVariants() ) {
2365 $user->setOption( 'variant', $wgContLang->getPreferredVariant() );
2366 }
2367 }
2368
2369 /**
2370 * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary
2371 * @param string $method
2372 * @param array $args
2373 */
2374 private function callMethodOnProviders( $which, $method, array $args ) {
2375 $providers = [];
2376 if ( $which & 1 ) {
2377 $providers += $this->getPreAuthenticationProviders();
2378 }
2379 if ( $which & 2 ) {
2380 $providers += $this->getPrimaryAuthenticationProviders();
2381 }
2382 if ( $which & 4 ) {
2383 $providers += $this->getSecondaryAuthenticationProviders();
2384 }
2385 foreach ( $providers as $provider ) {
2386 call_user_func_array( [ $provider, $method ], $args );
2387 }
2388 }
2389
2390 /**
2391 * Reset the internal caching for unit testing
2392 */
2393 public static function resetCache() {
2394 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
2395 // @codeCoverageIgnoreStart
2396 throw new \MWException( __METHOD__ . ' may only be called from unit tests!' );
2397 // @codeCoverageIgnoreEnd
2398 }
2399
2400 self::$instance = null;
2401 }
2402
2403 /**@}*/
2404
2405 }
2406
2407 /**
2408 * For really cool vim folding this needs to be at the end:
2409 * vim: foldmarker=@{,@} foldmethod=marker
2410 */