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