public function provideAuthentication() { $user = \User::newFromName('UTSysop'); $id = $user->getId(); $name = $user->getName(); $rememberReq = new RememberMeAuthenticationRequest(); $rememberReq->action = AuthManager::ACTION_LOGIN; $req = $this->getMockForAbstractClass(AuthenticationRequest::class); $req->foobar = 'baz'; $restartResponse = AuthenticationResponse::newRestart($this->message('authmanager-authn-no-local-user')); $restartResponse->neededRequests = [$rememberReq]; $restartResponse2Pass = AuthenticationResponse::newPass(null); $restartResponse2Pass->linkRequest = $req; $restartResponse2 = AuthenticationResponse::newRestart($this->message('authmanager-authn-no-local-user-link')); $restartResponse2->createRequest = new CreateFromLoginAuthenticationRequest(null, [$req->getUniqueId() => $req]); $restartResponse2->neededRequests = [$rememberReq, $restartResponse2->createRequest]; return ['Failure in pre-auth' => [StatusValue::newFatal('fail-from-pre'), [], [], [AuthenticationResponse::newFail($this->message('fail-from-pre')), AuthenticationResponse::newFail($this->message('authmanager-authn-not-in-progress'))]], 'Failure in primary' => [StatusValue::newGood(), $tmp = [AuthenticationResponse::newFail($this->message('fail-from-primary'))], [], $tmp], 'All primary abstain' => [StatusValue::newGood(), [AuthenticationResponse::newAbstain()], [], [AuthenticationResponse::newFail($this->message('authmanager-authn-no-primary'))]], 'Primary UI, then redirect, then fail' => [StatusValue::newGood(), $tmp = [AuthenticationResponse::newUI([$req], $this->message('...')), AuthenticationResponse::newRedirect([$req], '/foo.html', ['foo' => 'bar']), AuthenticationResponse::newFail($this->message('fail-in-primary-continue'))], [], $tmp], 'Primary redirect, then abstain' => [StatusValue::newGood(), [$tmp = AuthenticationResponse::newRedirect([$req], '/foo.html', ['foo' => 'bar']), AuthenticationResponse::newAbstain()], [], [$tmp, new \DomainException('MockPrimaryAuthenticationProvider::continuePrimaryAuthentication() returned ABSTAIN')]], 'Primary UI, then pass with no local user' => [StatusValue::newGood(), [$tmp = AuthenticationResponse::newUI([$req], $this->message('...')), AuthenticationResponse::newPass(null)], [], [$tmp, $restartResponse]], 'Primary UI, then pass with no local user (link type)' => [StatusValue::newGood(), [$tmp = AuthenticationResponse::newUI([$req], $this->message('...')), $restartResponse2Pass], [], [$tmp, $restartResponse2], true], 'Primary pass with invalid username' => [StatusValue::newGood(), [AuthenticationResponse::newPass('<>')], [], [new \DomainException('MockPrimaryAuthenticationProvider returned an invalid username: <>')]], 'Secondary fail' => [StatusValue::newGood(), [AuthenticationResponse::newPass($name)], $tmp = [AuthenticationResponse::newFail($this->message('fail-in-secondary'))], $tmp], 'Secondary UI, then abstain' => [StatusValue::newGood(), [AuthenticationResponse::newPass($name)], [$tmp = AuthenticationResponse::newUI([$req], $this->message('...')), AuthenticationResponse::newAbstain()], [$tmp, AuthenticationResponse::newPass($name)]], 'Secondary pass' => [StatusValue::newGood(), [AuthenticationResponse::newPass($name)], [AuthenticationResponse::newPass()], [AuthenticationResponse::newPass($name)]]]; }
/** * Continue an authentication flow * * Return values are interpreted as follows: * - status FAIL: Authentication failed. If $response->createRequest is * set, that may be passed to self::beginAuthentication() or to * self::beginAccountCreation() (after adding a username, if necessary) * to preserve state. * - status REDIRECT: The client should be redirected to the contained URL, * new AuthenticationRequests should be made (if any), then * AuthManager::continueAuthentication() should be called. * - status UI: The client should be presented with a user interface for * the fields in the specified AuthenticationRequests, then new * AuthenticationRequests should be made, then * AuthManager::continueAuthentication() should be called. * - status RESTART: The user logged in successfully with a third-party * service, but the third-party credentials aren't attached to any local * account. This could be treated as a UI or a FAIL. * - status PASS: Authentication was successful. * * @param AuthenticationRequest[] $reqs * @return AuthenticationResponse */ public function continueAuthentication(array $reqs) { $session = $this->request->getSession(); try { if (!$session->canSetUser()) { // Caller should have called canAuthenticateNow() // @codeCoverageIgnoreStart throw new \LogicException('Authentication is not possible now'); // @codeCoverageIgnoreEnd } $state = $session->getSecret('AuthManager::authnState'); if (!is_array($state)) { return AuthenticationResponse::newFail(wfMessage('authmanager-authn-not-in-progress')); } $state['continueRequests'] = []; $guessUserName = $state['guessUserName']; foreach ($reqs as $req) { $req->returnToUrl = $state['returnToUrl']; } // Step 1: Choose an primary authentication provider, and call it until it succeeds. if ($state['primary'] === null) { // We haven't picked a PrimaryAuthenticationProvider yet // @codeCoverageIgnoreStart $guessUserName = null; foreach ($reqs as $req) { if ($req->username !== null && $req->username !== '') { if ($guessUserName === null) { $guessUserName = $req->username; } elseif ($guessUserName !== $req->username) { $guessUserName = null; break; } } } $state['guessUserName'] = $guessUserName; // @codeCoverageIgnoreEnd $state['reqs'] = $reqs; foreach ($this->getPrimaryAuthenticationProviders() as $id => $provider) { $res = $provider->beginPrimaryAuthentication($reqs); switch ($res->status) { case AuthenticationResponse::PASS: $state['primary'] = $id; $state['primaryResponse'] = $res; $this->logger->debug("Primary login with {$id} succeeded"); break 2; case AuthenticationResponse::FAIL: $this->logger->debug("Login failed in primary authentication by {$id}"); if ($res->createRequest || $state['maybeLink']) { $res->createRequest = new CreateFromLoginAuthenticationRequest($res->createRequest, $state['maybeLink']); } $this->callMethodOnProviders(7, 'postAuthentication', [User::newFromName($guessUserName) ?: null, $res]); $session->remove('AuthManager::authnState'); \Hooks::run('AuthManagerLoginAuthenticateAudit', [$res, null, $guessUserName]); return $res; case AuthenticationResponse::ABSTAIN: // Continue loop break; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug("Primary login with {$id} returned {$res->status}"); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::authnState', $state); return $res; // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart default: throw new \DomainException(get_class($provider) . "::beginPrimaryAuthentication() returned {$res->status}"); // @codeCoverageIgnoreEnd } } if ($state['primary'] === null) { $this->logger->debug('Login failed in primary authentication because no provider accepted'); $ret = AuthenticationResponse::newFail(wfMessage('authmanager-authn-no-primary')); $this->callMethodOnProviders(7, 'postAuthentication', [User::newFromName($guessUserName) ?: null, $ret]); $session->remove('AuthManager::authnState'); return $ret; } } elseif ($state['primaryResponse'] === null) { $provider = $this->getAuthenticationProvider($state['primary']); if (!$provider instanceof PrimaryAuthenticationProvider) { // Configuration changed? Force them to start over. // @codeCoverageIgnoreStart $ret = AuthenticationResponse::newFail(wfMessage('authmanager-authn-not-in-progress')); $this->callMethodOnProviders(7, 'postAuthentication', [User::newFromName($guessUserName) ?: null, $ret]); $session->remove('AuthManager::authnState'); return $ret; // @codeCoverageIgnoreEnd } $id = $provider->getUniqueId(); $res = $provider->continuePrimaryAuthentication($reqs); switch ($res->status) { case AuthenticationResponse::PASS: $state['primaryResponse'] = $res; $this->logger->debug("Primary login with {$id} succeeded"); break; case AuthenticationResponse::FAIL: $this->logger->debug("Login failed in primary authentication by {$id}"); if ($res->createRequest || $state['maybeLink']) { $res->createRequest = new CreateFromLoginAuthenticationRequest($res->createRequest, $state['maybeLink']); } $this->callMethodOnProviders(7, 'postAuthentication', [User::newFromName($guessUserName) ?: null, $res]); $session->remove('AuthManager::authnState'); \Hooks::run('AuthManagerLoginAuthenticateAudit', [$res, null, $guessUserName]); return $res; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug("Primary login with {$id} returned {$res->status}"); $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::authnState', $state); return $res; default: throw new \DomainException(get_class($provider) . "::continuePrimaryAuthentication() returned {$res->status}"); } } $res = $state['primaryResponse']; if ($res->username === null) { $provider = $this->getAuthenticationProvider($state['primary']); if (!$provider instanceof PrimaryAuthenticationProvider) { // Configuration changed? Force them to start over. // @codeCoverageIgnoreStart $ret = AuthenticationResponse::newFail(wfMessage('authmanager-authn-not-in-progress')); $this->callMethodOnProviders(7, 'postAuthentication', [User::newFromName($guessUserName) ?: null, $ret]); $session->remove('AuthManager::authnState'); return $ret; // @codeCoverageIgnoreEnd } if ($provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_LINK && $res->linkRequest && $this->getAuthenticationProvider(ConfirmLinkSecondaryAuthenticationProvider::class)) { $state['maybeLink'][$res->linkRequest->getUniqueId()] = $res->linkRequest; $msg = 'authmanager-authn-no-local-user-link'; } else { $msg = 'authmanager-authn-no-local-user'; } $this->logger->debug("Primary login with {$provider->getUniqueId()} succeeded, but returned no user"); $ret = AuthenticationResponse::newRestart(wfMessage($msg)); $ret->neededRequests = $this->getAuthenticationRequestsInternal(self::ACTION_LOGIN, [], $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders()); if ($res->createRequest || $state['maybeLink']) { $ret->createRequest = new CreateFromLoginAuthenticationRequest($res->createRequest, $state['maybeLink']); $ret->neededRequests[] = $ret->createRequest; } $session->setSecret('AuthManager::authnState', ['reqs' => [], 'primary' => null, 'primaryResponse' => null, 'secondary' => [], 'continueRequests' => $ret->neededRequests] + $state); return $ret; } // Step 2: Primary authentication succeeded, create the User object // (and add the user locally if necessary) $user = User::newFromName($res->username, 'usable'); if (!$user) { throw new \DomainException(get_class($provider) . " returned an invalid username: {$res->username}"); } if ($user->getId() === 0) { // User doesn't exist locally. Create it. $this->logger->info('Auto-creating {user} on login', ['user' => $user->getName()]); $status = $this->autoCreateUser($user, $state['primary'], false); if (!$status->isGood()) { $ret = AuthenticationResponse::newFail(Status::wrap($status)->getMessage('authmanager-authn-autocreate-failed')); $this->callMethodOnProviders(7, 'postAuthentication', [$user, $ret]); $session->remove('AuthManager::authnState'); \Hooks::run('AuthManagerLoginAuthenticateAudit', [$ret, $user, $user->getName()]); return $ret; } } // Step 3: Iterate over all the secondary authentication providers. $beginReqs = $state['reqs']; foreach ($this->getSecondaryAuthenticationProviders() as $id => $provider) { if (!isset($state['secondary'][$id])) { // This provider isn't started yet, so we pass it the set // of reqs from beginAuthentication instead of whatever // might have been used by a previous provider in line. $func = 'beginSecondaryAuthentication'; $res = $provider->beginSecondaryAuthentication($user, $beginReqs); } elseif (!$state['secondary'][$id]) { $func = 'continueSecondaryAuthentication'; $res = $provider->continueSecondaryAuthentication($user, $reqs); } else { continue; } switch ($res->status) { case AuthenticationResponse::PASS: $this->logger->debug("Secondary login with {$id} succeeded"); // fall through // fall through case AuthenticationResponse::ABSTAIN: $state['secondary'][$id] = true; break; case AuthenticationResponse::FAIL: $this->logger->debug("Login failed in secondary authentication by {$id}"); $this->callMethodOnProviders(7, 'postAuthentication', [$user, $res]); $session->remove('AuthManager::authnState'); \Hooks::run('AuthManagerLoginAuthenticateAudit', [$res, $user, $user->getName()]); return $res; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug("Secondary login with {$id} returned " . $res->status); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::authnState', $state); return $res; // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart default: throw new \DomainException(get_class($provider) . "::{$func}() returned {$res->status}"); // @codeCoverageIgnoreEnd } } // Step 4: Authentication complete! Set the user in the session and // clean up. $this->logger->info('Login for {user} succeeded', ['user' => $user->getName()]); $req = AuthenticationRequest::getRequestByClass($beginReqs, RememberMeAuthenticationRequest::class); $this->setSessionDataForUser($user, $req && $req->rememberMe); $ret = AuthenticationResponse::newPass($user->getName()); $this->callMethodOnProviders(7, 'postAuthentication', [$user, $ret]); $session->remove('AuthManager::authnState'); $this->removeAuthenticationSessionData(null); \Hooks::run('AuthManagerLoginAuthenticateAudit', [$ret, $user, $user->getName()]); return $ret; } catch (\Exception $ex) { $session->remove('AuthManager::authnState'); throw $ex; } }