Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like AuthManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use AuthManager, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 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() { |
||
| 122 | |||
| 123 | /** |
||
| 124 | * @param WebRequest $request |
||
| 125 | * @param Config $config |
||
| 126 | */ |
||
| 127 | public function __construct( WebRequest $request, Config $config ) { |
||
| 132 | |||
| 133 | /** |
||
| 134 | * @param LoggerInterface $logger |
||
| 135 | */ |
||
| 136 | public function setLogger( LoggerInterface $logger ) { |
||
| 139 | |||
| 140 | /** |
||
| 141 | * @return WebRequest |
||
| 142 | */ |
||
| 143 | public function getRequest() { |
||
| 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 ) { |
||
| 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 ) { |
||
| 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() { |
||
| 231 | |||
| 232 | /** |
||
| 233 | * Start an authentication flow |
||
| 234 | * |
||
| 235 | * In addition to the AuthenticationRequests returned by |
||
| 236 | * $this->getAuthenticationRequests(), a client might include a |
||
| 237 | * CreateFromLoginAuthenticationRequest from a previous login attempt to |
||
| 238 | * preserve state. |
||
| 239 | * |
||
| 240 | * Instead of the AuthenticationRequests returned by |
||
| 241 | * $this->getAuthenticationRequests(), a client might pass a |
||
| 242 | * CreatedAccountAuthenticationRequest from an account creation that just |
||
| 243 | * succeeded to log in to the just-created account. |
||
| 244 | * |
||
| 245 | * @param AuthenticationRequest[] $reqs |
||
| 246 | * @param string $returnToUrl Url that REDIRECT responses should eventually |
||
| 247 | * return to. |
||
| 248 | * @return AuthenticationResponse See self::continueAuthentication() |
||
| 249 | */ |
||
| 250 | public function beginAuthentication( array $reqs, $returnToUrl ) { |
||
| 251 | $session = $this->request->getSession(); |
||
| 252 | if ( !$session->canSetUser() ) { |
||
| 253 | // Caller should have called canAuthenticateNow() |
||
| 254 | $session->remove( 'AuthManager::authnState' ); |
||
| 255 | throw new \LogicException( 'Authentication is not possible now' ); |
||
| 256 | } |
||
| 257 | |||
| 258 | $guessUserName = null; |
||
| 259 | View Code Duplication | foreach ( $reqs as $req ) { |
|
| 260 | $req->returnToUrl = $returnToUrl; |
||
| 261 | // @codeCoverageIgnoreStart |
||
| 262 | if ( $req->username !== null && $req->username !== '' ) { |
||
| 263 | if ( $guessUserName === null ) { |
||
| 264 | $guessUserName = $req->username; |
||
| 265 | } elseif ( $guessUserName !== $req->username ) { |
||
| 266 | $guessUserName = null; |
||
| 267 | break; |
||
| 268 | } |
||
| 269 | } |
||
| 270 | // @codeCoverageIgnoreEnd |
||
| 271 | } |
||
| 272 | |||
| 273 | // Check for special-case login of a just-created account |
||
| 274 | $req = AuthenticationRequest::getRequestByClass( |
||
| 275 | $reqs, CreatedAccountAuthenticationRequest::class |
||
| 276 | ); |
||
| 277 | if ( $req ) { |
||
| 278 | if ( !in_array( $req, $this->createdAccountAuthenticationRequests, true ) ) { |
||
| 279 | throw new \LogicException( |
||
| 280 | 'CreatedAccountAuthenticationRequests are only valid on ' . |
||
| 281 | 'the same AuthManager that created the account' |
||
| 282 | ); |
||
| 283 | } |
||
| 284 | |||
| 285 | $user = User::newFromName( $req->username ); |
||
| 286 | // @codeCoverageIgnoreStart |
||
| 287 | if ( !$user ) { |
||
| 288 | throw new \UnexpectedValueException( |
||
| 289 | "CreatedAccountAuthenticationRequest had invalid username \"{$req->username}\"" |
||
| 290 | ); |
||
| 291 | } elseif ( $user->getId() != $req->id ) { |
||
| 292 | throw new \UnexpectedValueException( |
||
| 293 | "ID for \"{$req->username}\" was {$user->getId()}, expected {$req->id}" |
||
| 294 | ); |
||
| 295 | } |
||
| 296 | // @codeCoverageIgnoreEnd |
||
| 297 | |||
| 298 | $this->logger->info( 'Logging in {user} after account creation', [ |
||
| 299 | 'user' => $user->getName(), |
||
| 300 | ] ); |
||
| 301 | $ret = AuthenticationResponse::newPass( $user->getName() ); |
||
| 302 | $this->setSessionDataForUser( $user ); |
||
| 303 | $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); |
||
| 304 | $session->remove( 'AuthManager::authnState' ); |
||
| 305 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); |
||
| 306 | return $ret; |
||
| 307 | } |
||
| 308 | |||
| 309 | $this->removeAuthenticationSessionData( null ); |
||
| 310 | |||
| 311 | foreach ( $this->getPreAuthenticationProviders() as $provider ) { |
||
| 312 | $status = $provider->testForAuthentication( $reqs ); |
||
| 313 | if ( !$status->isGood() ) { |
||
| 314 | $this->logger->debug( 'Login failed in pre-authentication by ' . $provider->getUniqueId() ); |
||
| 315 | $ret = AuthenticationResponse::newFail( |
||
| 316 | Status::wrap( $status )->getMessage() |
||
| 317 | ); |
||
| 318 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 319 | [ User::newFromName( $guessUserName ) ?: null, $ret ] |
||
| 320 | ); |
||
| 321 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, null, $guessUserName ] ); |
||
| 322 | return $ret; |
||
| 323 | } |
||
| 324 | } |
||
| 325 | |||
| 326 | $state = [ |
||
| 327 | 'reqs' => $reqs, |
||
| 328 | 'returnToUrl' => $returnToUrl, |
||
| 329 | 'guessUserName' => $guessUserName, |
||
| 330 | 'primary' => null, |
||
| 331 | 'primaryResponse' => null, |
||
| 332 | 'secondary' => [], |
||
| 333 | 'maybeLink' => [], |
||
| 334 | 'continueRequests' => [], |
||
| 335 | ]; |
||
| 336 | |||
| 337 | // Preserve state from a previous failed login |
||
| 338 | $req = AuthenticationRequest::getRequestByClass( |
||
| 339 | $reqs, CreateFromLoginAuthenticationRequest::class |
||
| 340 | ); |
||
| 341 | if ( $req ) { |
||
| 342 | $state['maybeLink'] = $req->maybeLink; |
||
| 343 | } |
||
| 344 | |||
| 345 | $session = $this->request->getSession(); |
||
| 346 | $session->setSecret( 'AuthManager::authnState', $state ); |
||
| 347 | $session->persist(); |
||
| 348 | |||
| 349 | return $this->continueAuthentication( $reqs ); |
||
| 350 | } |
||
| 351 | |||
| 352 | /** |
||
| 353 | * Continue an authentication flow |
||
| 354 | * |
||
| 355 | * Return values are interpreted as follows: |
||
| 356 | * - status FAIL: Authentication failed. If $response->createRequest is |
||
| 357 | * set, that may be passed to self::beginAuthentication() or to |
||
| 358 | * self::beginAccountCreation() to preserve state. |
||
| 359 | * - status REDIRECT: The client should be redirected to the contained URL, |
||
| 360 | * new AuthenticationRequests should be made (if any), then |
||
| 361 | * AuthManager::continueAuthentication() should be called. |
||
| 362 | * - status UI: The client should be presented with a user interface for |
||
| 363 | * the fields in the specified AuthenticationRequests, then new |
||
| 364 | * AuthenticationRequests should be made, then |
||
| 365 | * AuthManager::continueAuthentication() should be called. |
||
| 366 | * - status RESTART: The user logged in successfully with a third-party |
||
| 367 | * service, but the third-party credentials aren't attached to any local |
||
| 368 | * account. This could be treated as a UI or a FAIL. |
||
| 369 | * - status PASS: Authentication was successful. |
||
| 370 | * |
||
| 371 | * @param AuthenticationRequest[] $reqs |
||
| 372 | * @return AuthenticationResponse |
||
| 373 | */ |
||
| 374 | public function continueAuthentication( array $reqs ) { |
||
| 375 | $session = $this->request->getSession(); |
||
| 376 | try { |
||
| 377 | if ( !$session->canSetUser() ) { |
||
| 378 | // Caller should have called canAuthenticateNow() |
||
| 379 | // @codeCoverageIgnoreStart |
||
| 380 | throw new \LogicException( 'Authentication is not possible now' ); |
||
| 381 | // @codeCoverageIgnoreEnd |
||
| 382 | } |
||
| 383 | |||
| 384 | $state = $session->getSecret( 'AuthManager::authnState' ); |
||
| 385 | if ( !is_array( $state ) ) { |
||
| 386 | return AuthenticationResponse::newFail( |
||
| 387 | wfMessage( 'authmanager-authn-not-in-progress' ) |
||
| 388 | ); |
||
| 389 | } |
||
| 390 | $state['continueRequests'] = []; |
||
| 391 | |||
| 392 | $guessUserName = $state['guessUserName']; |
||
| 393 | |||
| 394 | foreach ( $reqs as $req ) { |
||
| 395 | $req->returnToUrl = $state['returnToUrl']; |
||
| 396 | } |
||
| 397 | |||
| 398 | // Step 1: Choose an primary authentication provider, and call it until it succeeds. |
||
| 399 | |||
| 400 | if ( $state['primary'] === null ) { |
||
| 401 | // We haven't picked a PrimaryAuthenticationProvider yet |
||
| 402 | // @codeCoverageIgnoreStart |
||
| 403 | $guessUserName = null; |
||
| 404 | View Code Duplication | foreach ( $reqs as $req ) { |
|
| 405 | if ( $req->username !== null && $req->username !== '' ) { |
||
| 406 | if ( $guessUserName === null ) { |
||
| 407 | $guessUserName = $req->username; |
||
| 408 | } elseif ( $guessUserName !== $req->username ) { |
||
| 409 | $guessUserName = null; |
||
| 410 | break; |
||
| 411 | } |
||
| 412 | } |
||
| 413 | } |
||
| 414 | $state['guessUserName'] = $guessUserName; |
||
| 415 | // @codeCoverageIgnoreEnd |
||
| 416 | $state['reqs'] = $reqs; |
||
| 417 | |||
| 418 | foreach ( $this->getPrimaryAuthenticationProviders() as $id => $provider ) { |
||
| 419 | $res = $provider->beginPrimaryAuthentication( $reqs ); |
||
| 420 | switch ( $res->status ) { |
||
| 421 | case AuthenticationResponse::PASS; |
||
| 422 | $state['primary'] = $id; |
||
| 423 | $state['primaryResponse'] = $res; |
||
| 424 | $this->logger->debug( "Primary login with $id succeeded" ); |
||
| 425 | break 2; |
||
| 426 | View Code Duplication | case AuthenticationResponse::FAIL; |
|
| 427 | $this->logger->debug( "Login failed in primary authentication by $id" ); |
||
| 428 | if ( $res->createRequest || $state['maybeLink'] ) { |
||
| 429 | $res->createRequest = new CreateFromLoginAuthenticationRequest( |
||
| 430 | $res->createRequest, $state['maybeLink'] |
||
| 431 | ); |
||
| 432 | } |
||
| 433 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 434 | [ User::newFromName( $guessUserName ) ?: null, $res ] |
||
| 435 | ); |
||
| 436 | $session->remove( 'AuthManager::authnState' ); |
||
| 437 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] ); |
||
| 438 | return $res; |
||
| 439 | case AuthenticationResponse::ABSTAIN; |
||
| 440 | // Continue loop |
||
| 441 | break; |
||
| 442 | case AuthenticationResponse::REDIRECT; |
||
| 443 | View Code Duplication | case AuthenticationResponse::UI; |
|
| 444 | $this->logger->debug( "Primary login with $id returned $res->status" ); |
||
| 445 | $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); |
||
| 446 | $state['primary'] = $id; |
||
| 447 | $state['continueRequests'] = $res->neededRequests; |
||
| 448 | $session->setSecret( 'AuthManager::authnState', $state ); |
||
| 449 | return $res; |
||
| 450 | |||
| 451 | // @codeCoverageIgnoreStart |
||
| 452 | default: |
||
| 453 | throw new \DomainException( |
||
| 454 | get_class( $provider ) . "::beginPrimaryAuthentication() returned $res->status" |
||
| 455 | ); |
||
| 456 | // @codeCoverageIgnoreEnd |
||
| 457 | } |
||
| 458 | } |
||
| 459 | if ( $state['primary'] === null ) { |
||
| 460 | $this->logger->debug( 'Login failed in primary authentication because no provider accepted' ); |
||
| 461 | $ret = AuthenticationResponse::newFail( |
||
| 462 | wfMessage( 'authmanager-authn-no-primary' ) |
||
| 463 | ); |
||
| 464 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 465 | [ User::newFromName( $guessUserName ) ?: null, $ret ] |
||
| 466 | ); |
||
| 467 | $session->remove( 'AuthManager::authnState' ); |
||
| 468 | return $ret; |
||
| 469 | } |
||
| 470 | } elseif ( $state['primaryResponse'] === null ) { |
||
| 471 | $provider = $this->getAuthenticationProvider( $state['primary'] ); |
||
| 472 | View Code Duplication | if ( !$provider instanceof PrimaryAuthenticationProvider ) { |
|
| 473 | // Configuration changed? Force them to start over. |
||
| 474 | // @codeCoverageIgnoreStart |
||
| 475 | $ret = AuthenticationResponse::newFail( |
||
| 476 | wfMessage( 'authmanager-authn-not-in-progress' ) |
||
| 477 | ); |
||
| 478 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 479 | [ User::newFromName( $guessUserName ) ?: null, $ret ] |
||
| 480 | ); |
||
| 481 | $session->remove( 'AuthManager::authnState' ); |
||
| 482 | return $ret; |
||
| 483 | // @codeCoverageIgnoreEnd |
||
| 484 | } |
||
| 485 | $id = $provider->getUniqueId(); |
||
| 486 | $res = $provider->continuePrimaryAuthentication( $reqs ); |
||
| 487 | switch ( $res->status ) { |
||
| 488 | case AuthenticationResponse::PASS; |
||
| 489 | $state['primaryResponse'] = $res; |
||
| 490 | $this->logger->debug( "Primary login with $id succeeded" ); |
||
| 491 | break; |
||
| 492 | View Code Duplication | case AuthenticationResponse::FAIL; |
|
| 493 | $this->logger->debug( "Login failed in primary authentication by $id" ); |
||
| 494 | if ( $res->createRequest || $state['maybeLink'] ) { |
||
| 495 | $res->createRequest = new CreateFromLoginAuthenticationRequest( |
||
| 496 | $res->createRequest, $state['maybeLink'] |
||
| 497 | ); |
||
| 498 | } |
||
| 499 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 500 | [ User::newFromName( $guessUserName ) ?: null, $res ] |
||
| 501 | ); |
||
| 502 | $session->remove( 'AuthManager::authnState' ); |
||
| 503 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, null, $guessUserName ] ); |
||
| 504 | return $res; |
||
| 505 | case AuthenticationResponse::REDIRECT; |
||
| 506 | case AuthenticationResponse::UI; |
||
| 507 | $this->logger->debug( "Primary login with $id returned $res->status" ); |
||
| 508 | $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $guessUserName ); |
||
| 509 | $state['continueRequests'] = $res->neededRequests; |
||
| 510 | $session->setSecret( 'AuthManager::authnState', $state ); |
||
| 511 | return $res; |
||
| 512 | default: |
||
| 513 | throw new \DomainException( |
||
| 514 | get_class( $provider ) . "::continuePrimaryAuthentication() returned $res->status" |
||
| 515 | ); |
||
| 516 | } |
||
| 517 | } |
||
| 518 | |||
| 519 | $res = $state['primaryResponse']; |
||
| 520 | if ( $res->username === null ) { |
||
| 521 | $provider = $this->getAuthenticationProvider( $state['primary'] ); |
||
| 522 | View Code Duplication | if ( !$provider instanceof PrimaryAuthenticationProvider ) { |
|
| 523 | // Configuration changed? Force them to start over. |
||
| 524 | // @codeCoverageIgnoreStart |
||
| 525 | $ret = AuthenticationResponse::newFail( |
||
| 526 | wfMessage( 'authmanager-authn-not-in-progress' ) |
||
| 527 | ); |
||
| 528 | $this->callMethodOnProviders( 7, 'postAuthentication', |
||
| 529 | [ User::newFromName( $guessUserName ) ?: null, $ret ] |
||
| 530 | ); |
||
| 531 | $session->remove( 'AuthManager::authnState' ); |
||
| 532 | return $ret; |
||
| 533 | // @codeCoverageIgnoreEnd |
||
| 534 | } |
||
| 535 | |||
| 536 | if ( $provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK && |
||
| 537 | $res->linkRequest && |
||
| 538 | // don't confuse the user with an incorrect message if linking is disabled |
||
| 539 | $this->getAuthenticationProvider( ConfirmLinkSecondaryAuthenticationProvider::class ) |
||
| 540 | ) { |
||
| 541 | $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest; |
||
| 542 | $msg = 'authmanager-authn-no-local-user-link'; |
||
| 543 | } else { |
||
| 544 | $msg = 'authmanager-authn-no-local-user'; |
||
| 545 | } |
||
| 546 | $this->logger->debug( |
||
| 547 | "Primary login with {$provider->getUniqueId()} succeeded, but returned no user" |
||
| 548 | ); |
||
| 549 | $ret = AuthenticationResponse::newRestart( wfMessage( $msg ) ); |
||
| 550 | $ret->neededRequests = $this->getAuthenticationRequestsInternal( |
||
| 551 | self::ACTION_LOGIN, |
||
| 552 | [], |
||
| 553 | $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders() |
||
| 554 | ); |
||
| 555 | View Code Duplication | if ( $res->createRequest || $state['maybeLink'] ) { |
|
| 556 | $ret->createRequest = new CreateFromLoginAuthenticationRequest( |
||
| 557 | $res->createRequest, $state['maybeLink'] |
||
| 558 | ); |
||
| 559 | $ret->neededRequests[] = $ret->createRequest; |
||
| 560 | } |
||
| 561 | $this->fillRequests( $ret->neededRequests, self::ACTION_LOGIN, null, true ); |
||
| 562 | $session->setSecret( 'AuthManager::authnState', [ |
||
| 563 | 'reqs' => [], // Will be filled in later |
||
| 564 | 'primary' => null, |
||
| 565 | 'primaryResponse' => null, |
||
| 566 | 'secondary' => [], |
||
| 567 | 'continueRequests' => $ret->neededRequests, |
||
| 568 | ] + $state ); |
||
| 569 | return $ret; |
||
| 570 | } |
||
| 571 | |||
| 572 | // Step 2: Primary authentication succeeded, create the User object |
||
| 573 | // (and add the user locally if necessary) |
||
| 574 | |||
| 575 | $user = User::newFromName( $res->username, 'usable' ); |
||
| 576 | if ( !$user ) { |
||
| 577 | throw new \DomainException( |
||
| 578 | get_class( $provider ) . " returned an invalid username: {$res->username}" |
||
| 579 | ); |
||
| 580 | } |
||
| 581 | if ( $user->getId() === 0 ) { |
||
| 582 | // User doesn't exist locally. Create it. |
||
| 583 | $this->logger->info( 'Auto-creating {user} on login', [ |
||
| 584 | 'user' => $user->getName(), |
||
| 585 | ] ); |
||
| 586 | $status = $this->autoCreateUser( $user, $state['primary'], false ); |
||
| 587 | if ( !$status->isGood() ) { |
||
| 588 | $ret = AuthenticationResponse::newFail( |
||
| 589 | Status::wrap( $status )->getMessage( 'authmanager-authn-autocreate-failed' ) |
||
| 590 | ); |
||
| 591 | $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); |
||
| 592 | $session->remove( 'AuthManager::authnState' ); |
||
| 593 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); |
||
| 594 | return $ret; |
||
| 595 | } |
||
| 596 | } |
||
| 597 | |||
| 598 | // Step 3: Iterate over all the secondary authentication providers. |
||
| 599 | |||
| 600 | $beginReqs = $state['reqs']; |
||
| 601 | |||
| 602 | foreach ( $this->getSecondaryAuthenticationProviders() as $id => $provider ) { |
||
| 603 | View Code Duplication | if ( !isset( $state['secondary'][$id] ) ) { |
|
| 604 | // This provider isn't started yet, so we pass it the set |
||
| 605 | // of reqs from beginAuthentication instead of whatever |
||
| 606 | // might have been used by a previous provider in line. |
||
| 607 | $func = 'beginSecondaryAuthentication'; |
||
| 608 | $res = $provider->beginSecondaryAuthentication( $user, $beginReqs ); |
||
| 609 | } elseif ( !$state['secondary'][$id] ) { |
||
| 610 | $func = 'continueSecondaryAuthentication'; |
||
| 611 | $res = $provider->continueSecondaryAuthentication( $user, $reqs ); |
||
| 612 | } else { |
||
| 613 | continue; |
||
| 614 | } |
||
| 615 | switch ( $res->status ) { |
||
| 616 | case AuthenticationResponse::PASS; |
||
| 617 | $this->logger->debug( "Secondary login with $id succeeded" ); |
||
| 618 | // fall through |
||
| 619 | case AuthenticationResponse::ABSTAIN; |
||
| 620 | $state['secondary'][$id] = true; |
||
| 621 | break; |
||
| 622 | View Code Duplication | case AuthenticationResponse::FAIL; |
|
| 623 | $this->logger->debug( "Login failed in secondary authentication by $id" ); |
||
| 624 | $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $res ] ); |
||
| 625 | $session->remove( 'AuthManager::authnState' ); |
||
| 626 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $res, $user, $user->getName() ] ); |
||
| 627 | return $res; |
||
| 628 | case AuthenticationResponse::REDIRECT; |
||
| 629 | View Code Duplication | case AuthenticationResponse::UI; |
|
| 630 | $this->logger->debug( "Secondary login with $id returned " . $res->status ); |
||
| 631 | $this->fillRequests( $res->neededRequests, self::ACTION_LOGIN, $user->getName() ); |
||
| 632 | $state['secondary'][$id] = false; |
||
| 633 | $state['continueRequests'] = $res->neededRequests; |
||
| 634 | $session->setSecret( 'AuthManager::authnState', $state ); |
||
| 635 | return $res; |
||
| 636 | |||
| 637 | // @codeCoverageIgnoreStart |
||
| 638 | default: |
||
| 639 | throw new \DomainException( |
||
| 640 | get_class( $provider ) . "::{$func}() returned $res->status" |
||
| 641 | ); |
||
| 642 | // @codeCoverageIgnoreEnd |
||
| 643 | } |
||
| 644 | } |
||
| 645 | |||
| 646 | // Step 4: Authentication complete! Set the user in the session and |
||
| 647 | // clean up. |
||
| 648 | |||
| 649 | $this->logger->info( 'Login for {user} succeeded', [ |
||
| 650 | 'user' => $user->getName(), |
||
| 651 | ] ); |
||
| 652 | $req = AuthenticationRequest::getRequestByClass( |
||
| 653 | $beginReqs, RememberMeAuthenticationRequest::class |
||
| 654 | ); |
||
| 655 | $this->setSessionDataForUser( $user, $req && $req->rememberMe ); |
||
| 656 | $ret = AuthenticationResponse::newPass( $user->getName() ); |
||
| 657 | $this->callMethodOnProviders( 7, 'postAuthentication', [ $user, $ret ] ); |
||
| 658 | $session->remove( 'AuthManager::authnState' ); |
||
| 659 | $this->removeAuthenticationSessionData( null ); |
||
| 660 | \Hooks::run( 'AuthManagerLoginAuthenticateAudit', [ $ret, $user, $user->getName() ] ); |
||
| 661 | return $ret; |
||
| 662 | } catch ( \Exception $ex ) { |
||
| 663 | $session->remove( 'AuthManager::authnState' ); |
||
| 664 | throw $ex; |
||
| 665 | } |
||
| 666 | } |
||
| 667 | |||
| 668 | /** |
||
| 669 | * Whether security-sensitive operations should proceed. |
||
| 670 | * |
||
| 671 | * A "security-sensitive operation" is something like a password or email |
||
| 672 | * change, that would normally have a "reenter your password to confirm" |
||
| 673 | * box if we only supported password-based authentication. |
||
| 674 | * |
||
| 675 | * @param string $operation Operation being checked. This should be a |
||
| 676 | * message-key-like string such as 'change-password' or 'change-email'. |
||
| 677 | * @return string One of the SEC_* constants. |
||
| 678 | */ |
||
| 679 | public function securitySensitiveOperationStatus( $operation ) { |
||
| 742 | |||
| 743 | /** |
||
| 744 | * Determine whether a username can authenticate |
||
| 745 | * |
||
| 746 | * @param string $username |
||
| 747 | * @return bool |
||
| 748 | */ |
||
| 749 | public function userCanAuthenticate( $username ) { |
||
| 757 | |||
| 758 | /** |
||
| 759 | * Provide normalized versions of the username for security checks |
||
| 760 | * |
||
| 761 | * Since different providers can normalize the input in different ways, |
||
| 762 | * this returns an array of all the different ways the name might be |
||
| 763 | * normalized for authentication. |
||
| 764 | * |
||
| 765 | * The returned strings should not be revealed to the user, as that might |
||
| 766 | * leak private information (e.g. an email address might be normalized to a |
||
| 767 | * username). |
||
| 768 | * |
||
| 769 | * @param string $username |
||
| 770 | * @return string[] |
||
| 771 | */ |
||
| 772 | public function normalizeUsername( $username ) { |
||
| 782 | |||
| 783 | /**@}*/ |
||
| 784 | |||
| 785 | /** |
||
| 786 | * @name Authentication data changing |
||
| 787 | * @{ |
||
| 788 | */ |
||
| 789 | |||
| 790 | /** |
||
| 791 | * Revoke any authentication credentials for a user |
||
| 792 | * |
||
| 793 | * After this, the user should no longer be able to log in. |
||
| 794 | * |
||
| 795 | * @param string $username |
||
| 796 | */ |
||
| 797 | public function revokeAccessForUser( $username ) { |
||
| 803 | |||
| 804 | /** |
||
| 805 | * Validate a change of authentication data (e.g. passwords) |
||
| 806 | * @param AuthenticationRequest $req |
||
| 807 | * @param bool $checkData If false, $req hasn't been loaded from the |
||
| 808 | * submission so checks on user-submitted fields should be skipped. $req->username is |
||
| 809 | * considered user-submitted for this purpose, even if it cannot be changed via |
||
| 810 | * $req->loadFromSubmission. |
||
| 811 | * @return Status |
||
| 812 | */ |
||
| 813 | public function allowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) { |
||
| 831 | |||
| 832 | /** |
||
| 833 | * Change authentication data (e.g. passwords) |
||
| 834 | * |
||
| 835 | * If $req was returned for AuthManager::ACTION_CHANGE, using $req should |
||
| 836 | * result in a successful login in the future. |
||
| 837 | * |
||
| 838 | * If $req was returned for AuthManager::ACTION_REMOVE, using $req should |
||
| 839 | * no longer result in a successful login. |
||
| 840 | * |
||
| 841 | * @param AuthenticationRequest $req |
||
| 842 | */ |
||
| 843 | public function changeAuthenticationData( AuthenticationRequest $req ) { |
||
| 855 | |||
| 856 | /**@}*/ |
||
| 857 | |||
| 858 | /** |
||
| 859 | * @name Account creation |
||
| 860 | * @{ |
||
| 861 | */ |
||
| 862 | |||
| 863 | /** |
||
| 864 | * Determine whether accounts can be created |
||
| 865 | * @return bool |
||
| 866 | */ |
||
| 867 | View Code Duplication | public function canCreateAccounts() { |
|
| 877 | |||
| 878 | /** |
||
| 879 | * Determine whether a particular account can be created |
||
| 880 | * @param string $username |
||
| 881 | * @param array $options |
||
| 882 | * - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL |
||
| 883 | * - creating: (bool) For internal use only. Never specify this. |
||
| 884 | * @return Status |
||
| 885 | */ |
||
| 886 | public function canCreateAccount( $username, $options = [] ) { |
||
| 928 | |||
| 929 | /** |
||
| 930 | * Basic permissions checks on whether a user can create accounts |
||
| 931 | * @param User $creator User doing the account creation |
||
| 932 | * @return Status |
||
| 933 | */ |
||
| 934 | public function checkAccountCreatePermissions( User $creator ) { |
||
| 976 | |||
| 977 | /** |
||
| 978 | * Start an account creation flow |
||
| 979 | * |
||
| 980 | * In addition to the AuthenticationRequests returned by |
||
| 981 | * $this->getAuthenticationRequests(), a client might include a |
||
| 982 | * CreateFromLoginAuthenticationRequest from a previous login attempt. If |
||
| 983 | * <code> |
||
| 984 | * $createFromLoginAuthenticationRequest->hasPrimaryStateForAction( AuthManager::ACTION_CREATE ) |
||
| 985 | * </code> |
||
| 986 | * returns true, any AuthenticationRequest::PRIMARY_REQUIRED requests |
||
| 987 | * should be omitted. If the CreateFromLoginAuthenticationRequest has a |
||
| 988 | * username set, that username must be used for all other requests. |
||
| 989 | * |
||
| 990 | * @param User $creator User doing the account creation |
||
| 991 | * @param AuthenticationRequest[] $reqs |
||
| 992 | * @param string $returnToUrl Url that REDIRECT responses should eventually |
||
| 993 | * return to. |
||
| 994 | * @return AuthenticationResponse |
||
| 995 | */ |
||
| 996 | public function beginAccountCreation( User $creator, array $reqs, $returnToUrl ) { |
||
| 1091 | |||
| 1092 | /** |
||
| 1093 | * Continue an account creation flow |
||
| 1094 | * @param AuthenticationRequest[] $reqs |
||
| 1095 | * @return AuthenticationResponse |
||
| 1096 | */ |
||
| 1097 | public function continueAccountCreation( array $reqs ) { |
||
| 1480 | |||
| 1481 | /** |
||
| 1482 | * Auto-create an account, and log into that account |
||
| 1483 | * @param User $user User to auto-create |
||
| 1484 | * @param string $source What caused the auto-creation? This must be the ID |
||
| 1485 | * of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION. |
||
| 1486 | * @param bool $login Whether to also log the user in |
||
| 1487 | * @return Status Good if user was created, Ok if user already existed, otherwise Fatal |
||
| 1488 | */ |
||
| 1489 | public function autoCreateUser( User $user, $source, $login = true ) { |
||
| 1704 | |||
| 1705 | /**@}*/ |
||
| 1706 | |||
| 1707 | /** |
||
| 1708 | * @name Account linking |
||
| 1709 | * @{ |
||
| 1710 | */ |
||
| 1711 | |||
| 1712 | /** |
||
| 1713 | * Determine whether accounts can be linked |
||
| 1714 | * @return bool |
||
| 1715 | */ |
||
| 1716 | View Code Duplication | public function canLinkAccounts() { |
|
| 1724 | |||
| 1725 | /** |
||
| 1726 | * Start an account linking flow |
||
| 1727 | * |
||
| 1728 | * @param User $user User being linked |
||
| 1729 | * @param AuthenticationRequest[] $reqs |
||
| 1730 | * @param string $returnToUrl Url that REDIRECT responses should eventually |
||
| 1731 | * return to. |
||
| 1732 | * @return AuthenticationResponse |
||
| 1733 | */ |
||
| 1734 | public function beginAccountLink( User $user, array $reqs, $returnToUrl ) { |
||
| 1837 | |||
| 1838 | /** |
||
| 1839 | * Continue an account linking flow |
||
| 1840 | * @param AuthenticationRequest[] $reqs |
||
| 1841 | * @return AuthenticationResponse |
||
| 1842 | */ |
||
| 1843 | public function continueAccountLink( array $reqs ) { |
||
| 1929 | |||
| 1930 | /**@}*/ |
||
| 1931 | |||
| 1932 | /** |
||
| 1933 | * @name Information methods |
||
| 1934 | * @{ |
||
| 1935 | */ |
||
| 1936 | |||
| 1937 | /** |
||
| 1938 | * Return the applicable list of AuthenticationRequests |
||
| 1939 | * |
||
| 1940 | * Possible values for $action: |
||
| 1941 | * - ACTION_LOGIN: Valid for passing to beginAuthentication |
||
| 1942 | * - ACTION_LOGIN_CONTINUE: Valid for passing to continueAuthentication in the current state |
||
| 1943 | * - ACTION_CREATE: Valid for passing to beginAccountCreation |
||
| 1944 | * - ACTION_CREATE_CONTINUE: Valid for passing to continueAccountCreation in the current state |
||
| 1945 | * - ACTION_LINK: Valid for passing to beginAccountLink |
||
| 1946 | * - ACTION_LINK_CONTINUE: Valid for passing to continueAccountLink in the current state |
||
| 1947 | * - ACTION_CHANGE: Valid for passing to changeAuthenticationData to change credentials |
||
| 1948 | * - ACTION_REMOVE: Valid for passing to changeAuthenticationData to remove credentials. |
||
| 1949 | * - ACTION_UNLINK: Same as ACTION_REMOVE, but limited to linked accounts. |
||
| 1950 | * |
||
| 1951 | * @param string $action One of the AuthManager::ACTION_* constants |
||
| 1952 | * @param User|null $user User being acted on, instead of the current user. |
||
| 1953 | * @return AuthenticationRequest[] |
||
| 1954 | */ |
||
| 1955 | public function getAuthenticationRequests( $action, User $user = null ) { |
||
| 2009 | |||
| 2010 | /** |
||
| 2011 | * Internal request lookup for self::getAuthenticationRequests |
||
| 2012 | * |
||
| 2013 | * @param string $providerAction Action to pass to providers |
||
| 2014 | * @param array $options Options to pass to providers |
||
| 2015 | * @param AuthenticationProvider[] $providers |
||
| 2016 | * @param User|null $user |
||
| 2017 | * @return AuthenticationRequest[] |
||
| 2018 | */ |
||
| 2019 | private function getAuthenticationRequestsInternal( |
||
| 2088 | |||
| 2089 | /** |
||
| 2090 | * Set values in an array of requests |
||
| 2091 | * @param AuthenticationRequest[] &$reqs |
||
| 2092 | * @param string $action |
||
| 2093 | * @param string|null $username |
||
| 2094 | * @param boolean $forceAction |
||
| 2095 | */ |
||
| 2096 | private function fillRequests( array &$reqs, $action, $username, $forceAction = false ) { |
||
| 2106 | |||
| 2107 | /** |
||
| 2108 | * Determine whether a username exists |
||
| 2109 | * @param string $username |
||
| 2110 | * @param int $flags Bitfield of User:READ_* constants |
||
| 2111 | * @return bool |
||
| 2112 | */ |
||
| 2113 | public function userExists( $username, $flags = User::READ_NORMAL ) { |
||
| 2122 | |||
| 2123 | /** |
||
| 2124 | * Determine whether a user property should be allowed to be changed. |
||
| 2125 | * |
||
| 2126 | * Supported properties are: |
||
| 2127 | * - emailaddress |
||
| 2128 | * - realname |
||
| 2129 | * - nickname |
||
| 2130 | * |
||
| 2131 | * @param string $property |
||
| 2132 | * @return bool |
||
| 2133 | */ |
||
| 2134 | public function allowsPropertyChange( $property ) { |
||
| 2144 | |||
| 2145 | /** |
||
| 2146 | * Get a provider by ID |
||
| 2147 | * @note This is public so extensions can check whether their own provider |
||
| 2148 | * is installed and so they can read its configuration if necessary. |
||
| 2149 | * Other uses are not recommended. |
||
| 2150 | * @param string $id |
||
| 2151 | * @return AuthenticationProvider|null |
||
| 2152 | */ |
||
| 2153 | public function getAuthenticationProvider( $id ) { |
||
| 2175 | |||
| 2176 | /**@}*/ |
||
| 2177 | |||
| 2178 | /** |
||
| 2179 | * @name Internal methods |
||
| 2180 | * @{ |
||
| 2181 | */ |
||
| 2182 | |||
| 2183 | /** |
||
| 2184 | * Store authentication in the current session |
||
| 2185 | * @protected For use by AuthenticationProviders |
||
| 2186 | * @param string $key |
||
| 2187 | * @param mixed $data Must be serializable |
||
| 2188 | */ |
||
| 2189 | public function setAuthenticationSessionData( $key, $data ) { |
||
| 2198 | |||
| 2199 | /** |
||
| 2200 | * Fetch authentication data from the current session |
||
| 2201 | * @protected For use by AuthenticationProviders |
||
| 2202 | * @param string $key |
||
| 2203 | * @param mixed $default |
||
| 2204 | * @return mixed |
||
| 2205 | */ |
||
| 2206 | public function getAuthenticationSessionData( $key, $default = null ) { |
||
| 2214 | |||
| 2215 | /** |
||
| 2216 | * Remove authentication data |
||
| 2217 | * @protected For use by AuthenticationProviders |
||
| 2218 | * @param string|null $key If null, all data is removed |
||
| 2219 | */ |
||
| 2220 | public function removeAuthenticationSessionData( $key ) { |
||
| 2232 | |||
| 2233 | /** |
||
| 2234 | * Create an array of AuthenticationProviders from an array of ObjectFactory specs |
||
| 2235 | * @param string $class |
||
| 2236 | * @param array[] $specs |
||
| 2237 | * @return AuthenticationProvider[] |
||
| 2238 | */ |
||
| 2239 | protected function providerArrayFromSpecs( $class, array $specs ) { |
||
| 2274 | |||
| 2275 | /** |
||
| 2276 | * Get the configuration |
||
| 2277 | * @return array |
||
| 2278 | */ |
||
| 2279 | private function getConfiguration() { |
||
| 2282 | |||
| 2283 | /** |
||
| 2284 | * Get the list of PreAuthenticationProviders |
||
| 2285 | * @return PreAuthenticationProvider[] |
||
| 2286 | */ |
||
| 2287 | protected function getPreAuthenticationProviders() { |
||
| 2296 | |||
| 2297 | /** |
||
| 2298 | * Get the list of PrimaryAuthenticationProviders |
||
| 2299 | * @return PrimaryAuthenticationProvider[] |
||
| 2300 | */ |
||
| 2301 | protected function getPrimaryAuthenticationProviders() { |
||
| 2310 | |||
| 2311 | /** |
||
| 2312 | * Get the list of SecondaryAuthenticationProviders |
||
| 2313 | * @return SecondaryAuthenticationProvider[] |
||
| 2314 | */ |
||
| 2315 | protected function getSecondaryAuthenticationProviders() { |
||
| 2324 | |||
| 2325 | /** |
||
| 2326 | * @param User $user |
||
| 2327 | * @param bool|null $remember |
||
| 2328 | */ |
||
| 2329 | private function setSessionDataForUser( $user, $remember = null ) { |
||
| 2349 | |||
| 2350 | /** |
||
| 2351 | * @param User $user |
||
| 2352 | * @param bool $useContextLang Use 'uselang' to set the user's language |
||
| 2353 | */ |
||
| 2354 | private function setDefaultUserOptions( User $user, $useContextLang ) { |
||
| 2366 | |||
| 2367 | /** |
||
| 2368 | * @param int $which Bitmask: 1 = pre, 2 = primary, 4 = secondary |
||
| 2369 | * @param string $method |
||
| 2370 | * @param array $args |
||
| 2371 | */ |
||
| 2372 | private function callMethodOnProviders( $which, $method, array $args ) { |
||
| 2387 | |||
| 2388 | /** |
||
| 2389 | * Reset the internal caching for unit testing |
||
| 2390 | */ |
||
| 2391 | public static function resetCache() { |
||
| 2400 | |||
| 2401 | /**@}*/ |
||
| 2402 | |||
| 2403 | } |
||
| 2404 | |||
| 2409 |
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.