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