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