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