public function testForAuthentication(array $reqs) { $req = AuthenticationRequest::getRequestByClass($reqs, PasswordAuthenticationRequest::class); if ($req) { $user = User::newFromName($req->username); $password = $req->password; } else { $user = null; foreach ($reqs as $req) { if ($req->username !== null) { $user = User::newFromName($req->username); break; } } if (!$user) { $this->logger->debug(__METHOD__ . ': No username in $reqs, skipping hooks'); return StatusValue::newGood(); } // Something random for the 'AbortLogin' hook. $password = wfRandomString(32); } $msg = null; if (!\Hooks::run('LoginUserMigrated', [$user, &$msg])) { return $this->makeFailResponse($user, null, LoginForm::USER_MIGRATED, $msg, 'LoginUserMigrated'); } $abort = LoginForm::ABORTED; $msg = null; if (!\Hooks::run('AbortLogin', [$user, $password, &$abort, &$msg])) { return $this->makeFailResponse($user, null, $abort, $msg, 'AbortLogin'); } return StatusValue::newGood(); }
public function beginPrimaryAccountCreation($user, $creator, array $reqs) { /** @var TemporaryPasswordAuthenticationRequest $req */ $req = AuthenticationRequest::getRequestByClass($reqs, TemporaryPasswordAuthenticationRequest::class); if ($req) { if ($req->username !== null && $req->password !== null) { // Nothing we can do yet, because the user isn't in the DB yet if ($req->username !== $user->getName()) { $req = clone $req; $req->username = $user->getName(); } if ($req->mailpassword) { // prevent EmailNotificationSecondaryAuthenticationProvider from sending another mail $this->manager->setAuthenticationSessionData('no-email', true); } $ret = AuthenticationResponse::newPass($req->username); $ret->createRequest = $req; return $ret; } } return AuthenticationResponse::newAbstain(); }
public function handleFormSubmit($data) { // remove requests do not accept user input $requests = $this->authRequests; if (static::$loadUserData) { $requests = AuthenticationRequest::loadRequestsFromSubmission($this->authRequests, $data); } $response = $this->performAuthenticationStep($this->authAction, $requests); // we can't handle FAIL or similar as failure here since it might require changing the form return Status::newGood($response); }
public function beginPrimaryAccountCreation($user, $creator, array $reqs) { if ($this->accountCreationType() === self::TYPE_NONE) { throw new \BadMethodCallException('Shouldn\'t call this when accountCreationType() is NONE'); } $req = AuthenticationRequest::getRequestByClass($reqs, PasswordAuthenticationRequest::class); if ($req) { if ($req->username !== null && $req->password !== null) { // Nothing we can do besides claim it, because the user isn't in // the DB yet if ($req->username !== $user->getName()) { $req = clone $req; $req->username = $user->getName(); } $ret = AuthenticationResponse::newPass($req->username); $ret->createRequest = $req; return $ret; } } return AuthenticationResponse::newAbstain(); }
/** * Checks whether the given AuthenticationRequest has its own submit button. * @param AuthenticationRequest $req * @return bool */ protected function hasOwnSubmitButton(AuthenticationRequest $req) { foreach ($req->getFieldInfo() as $field => $info) { if ($info['type'] === 'button') { return true; } } return false; }
/** * Executes the log-in attempt using the parameters passed. If * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { // If we're in a mode that breaks the same-origin policy, no tokens can // be obtained if ($this->lacksSameOriginSecurity()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when the same-origin policy is not applied']); return; } try { $this->requirePostedParameters(['password', 'token']); } catch (UsageException $ex) { // Make this a warning for now, upgrade to an error in 1.29. $this->setWarning($ex->getMessage()); $this->logFeatureUsage('login-params-in-query-string'); } $params = $this->extractRequestParams(); $result = []; // Make sure session is persisted $session = MediaWiki\Session\SessionManager::getGlobalSession(); $session->persist(); // Make sure it's possible to log in if (!$session->canSetUser()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when using ' . $session->getProvider()->describe(Language::factory('en'))]); return; } $authRes = false; $context = new DerivativeContext($this->getContext()); $loginType = 'N/A'; // Check login token $token = $session->getToken('', 'login'); if ($token->wasNew() || !$params['token']) { $authRes = 'NeedToken'; } elseif (!$token->match($params['token'])) { $authRes = 'WrongToken'; } // Try bot passwords if ($authRes === false && $this->getConfig()->get('EnableBotPasswords') && ($botLoginData = BotPassword::canonicalizeLoginData($params['name'], $params['password']))) { $status = BotPassword::login($botLoginData[0], $botLoginData[1], $this->getRequest()); if ($status->isOK()) { $session = $status->getValue(); $authRes = 'Success'; $loginType = 'BotPassword'; } elseif (!$botLoginData[2]) { $authRes = 'Failed'; $message = $status->getMessage(); LoggerFactory::getInstance('authentication')->info('BotPassword login failed: ' . $status->getWikiText(false, false, 'en')); } } if ($authRes === false) { // Simplified AuthManager login, for backwards compatibility $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN, $this->getUser()), ['username' => $params['name'], 'password' => $params['password'], 'domain' => $params['domain'], 'rememberMe' => true]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: if ($this->getConfig()->get('EnableBotPasswords')) { $warn = 'Main-account login via action=login is deprecated and may stop working ' . 'without warning.'; $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].'; $warn .= ' To safely continue using main-account login, see action=clientlogin.'; } else { $warn = 'Login via action=login is deprecated and may stop working without warning.'; $warn .= ' To safely log in, see action=clientlogin.'; } $this->setWarning($warn); $authRes = 'Success'; $loginType = 'AuthManager'; break; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... $authRes = 'Failed'; $message = $res->message; \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $message->inLanguage('en')->plain()); break; default: \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed due to unsupported response type: ' . $res->status, $this->getAuthenticationResponseLogData($res)); $authRes = 'Aborted'; break; } } $result['result'] = $authRes; switch ($authRes) { case 'Success': $user = $session->getUser(); ApiQueryInfo::resetTokenCache(); // Deprecated hook $injected_html = ''; Hooks::run('UserLoginComplete', [&$user, &$injected_html, true]); $result['lguserid'] = intval($user->getId()); $result['lgusername'] = $user->getName(); break; case 'NeedToken': $result['token'] = $token->toString(); $this->setWarning('Fetching a token via action=login is deprecated. ' . 'Use action=query&meta=tokens&type=login instead.'); $this->logFeatureUsage('action=login&!lgtoken'); break; case 'WrongToken': break; case 'Failed': $result['reason'] = $message->useDatabase('false')->inLanguage('en')->text(); break; case 'Aborted': $result['reason'] = 'Authentication requires user interaction, ' . 'which is not supported by action=login.'; if ($this->getConfig()->get('EnableBotPasswords')) { $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].'; $result['reason'] .= ' To continue using main-account login, see action=clientlogin.'; } else { $result['reason'] .= ' To log in, see action=clientlogin.'; } break; default: ApiBase::dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } $this->getResult()->addValue(null, 'login', $result); if ($loginType === 'LoginForm' && isset(LoginForm::$statusCodes[$authRes])) { $authRes = LoginForm::$statusCodes[$authRes]; } LoggerFactory::getInstance('authevents')->info('Login attempt', ['event' => 'login', 'successful' => $authRes === 'Success', 'loginType' => $loginType, 'status' => $authRes]); }
public function beginPrimaryAccountCreation($user, $creator, array $reqs) { if ($this->accountCreationType() === self::TYPE_NONE) { throw new \BadMethodCallException('Shouldn\'t call this when accountCreationType() is NONE'); } $req = AuthenticationRequest::getRequestByClass($reqs, $this->requestType); if (!$req || $req->username === null || $req->password === null || $this->hasDomain && $req->domain === null) { return AuthenticationResponse::newAbstain(); } $username = User::getCanonicalName($req->username, 'usable'); if ($username === false) { return AuthenticationResponse::newAbstain(); } $this->setDomain($req); if ($this->auth->addUser($user, $req->password, $user->getEmail(), $user->getRealName())) { return AuthenticationResponse::newPass(); } else { return AuthenticationResponse::newFail(new \Message('authmanager-authplugin-create-fail')); } }
/** * Generates a form from the given request. * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name * @param string|Message $msg * @param string $msgType * @return HTMLForm */ protected function getAuthForm(array $requests, $action, $msg = '', $msgType = 'error') { global $wgSecureLogin, $wgLoginLanguageSelector; // FIXME merge this with parent if (isset($this->authForm)) { return $this->authForm; } $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; // get basic form description from the auth logic $fieldInfo = AuthenticationRequest::mergeFieldInfo($requests); $fakeTemplate = $this->getFakeTemplate($msg, $msgType); $this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook // this will call onAuthChangeFormFields() $formDescriptor = static::fieldInfoToFormDescriptor($requests, $fieldInfo, $this->authAction); $this->postProcessFormDescriptor($formDescriptor); $context = $this->getContext(); if ($context->getRequest() !== $this->getRequest()) { // We have overridden the request, need to make sure the form uses that too. $context = new DerivativeContext($this->getContext()); $context->setRequest($this->getRequest()); } $form = HTMLForm::factory('vform', $formDescriptor, $context); $form->addHiddenField('authAction', $this->authAction); if ($wgLoginLanguageSelector) { $form->addHiddenField('uselang', $this->mLanguage); } $form->addHiddenField('force', $this->securityLevel); $form->addHiddenField($this->getTokenName(), $this->getToken()->toString()); if ($wgSecureLogin) { // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved if (!$this->isSignup()) { $form->addHiddenField('wpForceHttps', (int) $this->mStickHTTPS); $form->addHiddenField('wpFromhttp', $usingHTTPS); } } // set properties of the form itself $form->setAction($this->getPageTitle()->getLocalURL($this->getReturnToQueryStringFragment())); $form->setName('userlogin' . ($this->isSignup() ? '2' : '')); if ($this->isSignup()) { $form->setId('userlogin2'); } // add pre/post text // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup // should be above the error message but HTMLForm doesn't support that $form->addHeaderText($fakeTemplate->html('header')); // FIXME the old form used this for error/warning messages which does not play well with // HTMLForm (maybe it could with a subclass?); for now only display it for signups // (where the JS username validation needs it) and alway empty if ($this->isSignup()) { // used by the mediawiki.special.userlogin.signup.js module $statusAreaAttribs = ['id' => 'mw-createacct-status-area']; // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ]; $form->addHeaderText(Html::element('div', $statusAreaAttribs)); } // header used by MobileFrontend $form->addHeaderText($fakeTemplate->html('formheader')); // blank signup footer for site customization if ($this->isSignup() && $this->showExtraInformation()) { // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise $signupendMsg = $this->msg('signupend'); $signupendHttpsMsg = $this->msg('signupend-https'); if (!$signupendMsg->isDisabled()) { $signupendText = $usingHTTPS && !$signupendHttpsMsg->isBlank() ? $signupendHttpsMsg->parse() : $signupendMsg->parse(); $form->addPostText(Html::rawElement('div', ['id' => 'signupend'], $signupendText)); } } // warning header for non-standard workflows (e.g. security reauthentication) if (!$this->isSignup() && $this->getUser()->isLoggedIn()) { $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin'; $form->addHeaderText(Html::rawElement('div', ['class' => 'warningbox'], $this->msg($reauthMessage)->params($this->getUser()->getName())->parse())); } if (!$this->isSignup() && $this->showExtraInformation()) { $passwordReset = new PasswordReset($this->getConfig(), AuthManager::singleton()); if ($passwordReset->isAllowed($this->getUser())) { $form->addFooterText(Html::rawElement('div', ['class' => 'mw-ui-vform-field mw-form-related-link-container'], Linker::link(SpecialPage::getTitleFor('PasswordReset'), $this->msg('userlogin-resetpassword-link')->escaped()))); } // Don't show a "create account" link if the user can't. if ($this->showCreateAccountLink()) { // link to the other action $linkTitle = $this->getTitleFor($this->isSignup() ? 'Userlogin' : 'CreateAccount'); $linkq = $this->getReturnToQueryStringFragment(); // Pass any language selection on to the mode switch link if ($wgLoginLanguageSelector && $this->mLanguage) { $linkq .= '&uselang=' . $this->mLanguage; } $createOrLoginHref = $linkTitle->getLocalURL($linkq); if ($this->getUser()->isLoggedIn()) { $createOrLoginHtml = Html::rawElement('div', ['class' => 'mw-ui-vform-field'], Html::element('a', ['id' => 'mw-createaccount-join', 'href' => $createOrLoginHref, 'tabindex' => 100], $this->msg('userlogin-createanother')->escaped())); } else { $createOrLoginHtml = Html::rawElement('div', ['id' => 'mw-createaccount-cta', 'class' => 'mw-ui-vform-field'], $this->msg('userlogin-noaccount')->escaped() . Html::element('a', ['id' => 'mw-createaccount-join', 'href' => $createOrLoginHref, 'class' => 'mw-ui-button', 'tabindex' => 100], $this->msg('userlogin-joinproject')->escaped())); } $form->addFooterText($createOrLoginHtml); } } $form->suppressDefaultSubmit(); $this->authForm = $form; return $form; }
/** * Format an array of AuthenticationRequests for return * @param AuthenticationRequest[] $reqs * @return array Will have a 'requests' key, and also 'fields' if $module's * params include 'mergerequestfields'. */ public function formatRequests(array $reqs) { $params = $this->module->extractRequestParams(); $mergeFields = !empty($params['mergerequestfields']); $ret = ['requests' => []]; foreach ($reqs as $req) { $describe = $req->describeCredentials(); $reqInfo = ['id' => $req->getUniqueId(), 'metadata' => $req->getMetadata()]; switch ($req->required) { case AuthenticationRequest::OPTIONAL: $reqInfo['required'] = 'optional'; break; case AuthenticationRequest::REQUIRED: $reqInfo['required'] = 'required'; break; case AuthenticationRequest::PRIMARY_REQUIRED: $reqInfo['required'] = 'primary-required'; break; } $this->formatMessage($reqInfo, 'provider', $describe['provider']); $this->formatMessage($reqInfo, 'account', $describe['account']); if (!$mergeFields) { $reqInfo['fields'] = $this->formatFields((array) $req->getFieldInfo()); } $ret['requests'][] = $reqInfo; } if ($mergeFields) { $fields = AuthenticationRequest::mergeFieldInfo($reqs); $ret['fields'] = $this->formatFields($fields); } return $ret; }
/** * Continue an account creation flow * @param AuthenticationRequest[] $reqs * @return AuthenticationResponse */ public function continueAccountCreation(array $reqs) { $session = $this->request->getSession(); try { if (!$this->canCreateAccounts()) { // Caller should have called canCreateAccounts() $session->remove('AuthManager::accountCreationState'); throw new \LogicException('Account creation is not possible'); } $state = $session->getSecret('AuthManager::accountCreationState'); if (!is_array($state)) { return AuthenticationResponse::newFail(wfMessage('authmanager-create-not-in-progress')); } $state['continueRequests'] = []; // Step 0: Prepare and validate the input $user = User::newFromName($state['username'], 'creatable'); if (!is_object($user)) { $session->remove('AuthManager::accountCreationState'); $this->logger->debug(__METHOD__ . ': Invalid username', ['user' => $state['username']]); return AuthenticationResponse::newFail(wfMessage('noname')); } if ($state['creatorid']) { $creator = User::newFromId($state['creatorid']); } else { $creator = new User(); $creator->setName($state['creatorname']); } // Avoid account creation races on double submissions $cache = \ObjectCache::getLocalClusterInstance(); $lock = $cache->getScopedLock($cache->makeGlobalKey('account', md5($user->getName()))); if (!$lock) { // Don't clear AuthManager::accountCreationState for this code // path because the process that won the race owns it. $this->logger->debug(__METHOD__ . ': Could not acquire account creation lock', ['user' => $user->getName(), 'creator' => $creator->getName()]); return AuthenticationResponse::newFail(wfMessage('usernameinprogress')); } // Permissions check $status = $this->checkAccountCreatePermissions($creator); if (!$status->isGood()) { $this->logger->debug(__METHOD__ . ': {creator} cannot create users: {reason}', ['user' => $user->getName(), 'creator' => $creator->getName(), 'reason' => $status->getWikiText(null, null, 'en')]); $ret = AuthenticationResponse::newFail($status->getMessage()); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; } // Load from master for existence check $user->load(User::READ_LOCKING); if ($state['userid'] === 0) { if ($user->getId() != 0) { $this->logger->debug(__METHOD__ . ': User exists locally', ['user' => $user->getName(), 'creator' => $creator->getName()]); $ret = AuthenticationResponse::newFail(wfMessage('userexists')); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; } } else { if ($user->getId() == 0) { $this->logger->debug(__METHOD__ . ': User does not exist locally when it should', ['user' => $user->getName(), 'creator' => $creator->getName(), 'expected_id' => $state['userid']]); throw new \UnexpectedValueException("User \"{$state['username']}\" should exist now, but doesn't!"); } if ($user->getId() != $state['userid']) { $this->logger->debug(__METHOD__ . ': User ID/name mismatch', ['user' => $user->getName(), 'creator' => $creator->getName(), 'expected_id' => $state['userid'], 'actual_id' => $user->getId()]); throw new \UnexpectedValueException("User \"{$state['username']}\" exists, but " . "ID {$user->getId()} != {$state['userid']}!"); } } foreach ($state['reqs'] as $req) { if ($req instanceof UserDataAuthenticationRequest) { $status = $req->populateUser($user); if (!$status->isGood()) { // This should never happen... $status = Status::wrap($status); $this->logger->debug(__METHOD__ . ': UserData is invalid: {reason}', ['user' => $user->getName(), 'creator' => $creator->getName(), 'reason' => $status->getWikiText(null, null, 'en')]); $ret = AuthenticationResponse::newFail($status->getMessage()); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; } } } foreach ($reqs as $req) { $req->returnToUrl = $state['returnToUrl']; $req->username = $state['username']; } // If we're coming in from a create-from-login UI response, we need // to extract the createRequest (if any). $req = AuthenticationRequest::getRequestByClass($reqs, CreateFromLoginAuthenticationRequest::class); if ($req && $req->createRequest) { $reqs[] = $req->createRequest; } // Run pre-creation tests, if we haven't already if (!$state['ranPreTests']) { $providers = $this->getPreAuthenticationProviders() + $this->getPrimaryAuthenticationProviders() + $this->getSecondaryAuthenticationProviders(); foreach ($providers as $id => $provider) { $status = $provider->testForAccountCreation($user, $creator, $reqs); if (!$status->isGood()) { $this->logger->debug(__METHOD__ . ": Fail in pre-authentication by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $ret = AuthenticationResponse::newFail(Status::wrap($status)->getMessage()); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; } } $state['ranPreTests'] = true; } // Step 1: Choose a primary authentication provider and call it until it succeeds. if ($state['primary'] === null) { // We haven't picked a PrimaryAuthenticationProvider yet foreach ($this->getPrimaryAuthenticationProviders() as $id => $provider) { if ($provider->accountCreationType() === PrimaryAuthenticationProvider::TYPE_NONE) { continue; } $res = $provider->beginPrimaryAccountCreation($user, $creator, $reqs); switch ($res->status) { case AuthenticationResponse::PASS: $this->logger->debug(__METHOD__ . ": Primary creation passed by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $state['primary'] = $id; $state['primaryResponse'] = $res; break 2; case AuthenticationResponse::FAIL: $this->logger->debug(__METHOD__ . ": Primary creation failed by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $res]); $session->remove('AuthManager::accountCreationState'); return $res; case AuthenticationResponse::ABSTAIN: // Continue loop break; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug(__METHOD__ . ": Primary creation {$res->status} by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $state['primary'] = $id; $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::accountCreationState', $state); return $res; // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart default: throw new \DomainException(get_class($provider) . "::beginPrimaryAccountCreation() returned {$res->status}"); // @codeCoverageIgnoreEnd } } if ($state['primary'] === null) { $this->logger->debug(__METHOD__ . ': Primary creation failed because no provider accepted', ['user' => $user->getName(), 'creator' => $creator->getName()]); $ret = AuthenticationResponse::newFail(wfMessage('authmanager-create-no-primary')); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); 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-create-not-in-progress')); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; // @codeCoverageIgnoreEnd } $id = $provider->getUniqueId(); $res = $provider->continuePrimaryAccountCreation($user, $creator, $reqs); switch ($res->status) { case AuthenticationResponse::PASS: $this->logger->debug(__METHOD__ . ": Primary creation passed by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $state['primaryResponse'] = $res; break; case AuthenticationResponse::FAIL: $this->logger->debug(__METHOD__ . ": Primary creation failed by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $res]); $session->remove('AuthManager::accountCreationState'); return $res; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug(__METHOD__ . ": Primary creation {$res->status} by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::accountCreationState', $state); return $res; default: throw new \DomainException(get_class($provider) . "::continuePrimaryAccountCreation() returned {$res->status}"); } } // Step 2: Primary authentication succeeded, create the User object // and add the user locally. if ($state['userid'] === 0) { $this->logger->info('Creating user {user} during account creation', ['user' => $user->getName(), 'creator' => $creator->getName()]); $status = $user->addToDatabase(); if (!$status->isOk()) { // @codeCoverageIgnoreStart $ret = AuthenticationResponse::newFail($status->getMessage()); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); return $ret; // @codeCoverageIgnoreEnd } $this->setDefaultUserOptions($user, $creator->isAnon()); \Hooks::run('LocalUserCreated', [$user, false]); $user->saveSettings(); $state['userid'] = $user->getId(); // Update user count \DeferredUpdates::addUpdate(new \SiteStatsUpdate(0, 0, 0, 0, 1)); // Watch user's userpage and talk page $user->addWatch($user->getUserPage(), User::IGNORE_USER_RIGHTS); // Inform the provider $logSubtype = $provider->finishAccountCreation($user, $creator, $state['primaryResponse']); // Log the creation if ($this->config->get('NewUserLog')) { $isAnon = $creator->isAnon(); $logEntry = new \ManualLogEntry('newusers', $logSubtype ?: ($isAnon ? 'create' : 'create2')); $logEntry->setPerformer($isAnon ? $user : $creator); $logEntry->setTarget($user->getUserPage()); $req = AuthenticationRequest::getRequestByClass($state['reqs'], CreationReasonAuthenticationRequest::class); $logEntry->setComment($req ? $req->reason : ''); $logEntry->setParameters(['4::userid' => $user->getId()]); $logid = $logEntry->insert(); $logEntry->publish($logid); } } // 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 = 'beginSecondaryAccountCreation'; $res = $provider->beginSecondaryAccountCreation($user, $creator, $beginReqs); } elseif (!$state['secondary'][$id]) { $func = 'continueSecondaryAccountCreation'; $res = $provider->continueSecondaryAccountCreation($user, $creator, $reqs); } else { continue; } switch ($res->status) { case AuthenticationResponse::PASS: $this->logger->debug(__METHOD__ . ": Secondary creation passed by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); // fall through // fall through case AuthenticationResponse::ABSTAIN: $state['secondary'][$id] = true; break; case AuthenticationResponse::REDIRECT: case AuthenticationResponse::UI: $this->logger->debug(__METHOD__ . ": Secondary creation {$res->status} by {$id}", ['user' => $user->getName(), 'creator' => $creator->getName()]); $state['secondary'][$id] = false; $state['continueRequests'] = $res->neededRequests; $session->setSecret('AuthManager::accountCreationState', $state); return $res; case AuthenticationResponse::FAIL: throw new \DomainException(get_class($provider) . "::{$func}() returned {$res->status}." . ' Secondary providers are not allowed to fail account creation, that' . ' should have been done via testForAccountCreation().'); // @codeCoverageIgnoreStart // @codeCoverageIgnoreStart default: throw new \DomainException(get_class($provider) . "::{$func}() returned {$res->status}"); // @codeCoverageIgnoreEnd } } $id = $user->getId(); $name = $user->getName(); $req = new CreatedAccountAuthenticationRequest($id, $name); $ret = AuthenticationResponse::newPass($name); $ret->loginRequest = $req; $this->createdAccountAuthenticationRequests[] = $req; $this->logger->info(__METHOD__ . ': Account creation succeeded for {user}', ['user' => $user->getName(), 'creator' => $creator->getName()]); $this->callMethodOnProviders(7, 'postAccountCreation', [$user, $creator, $ret]); $session->remove('AuthManager::accountCreationState'); $this->removeAuthenticationSessionData(null); return $ret; } catch (\Exception $ex) { $session->remove('AuthManager::accountCreationState'); throw $ex; } }
public function addUser($user, $password, $email = '', $realname = '') { global $wgUser; $data = ['username' => $user->getName(), 'password' => $password, 'retype' => $password, 'email' => $email, 'realname' => $realname]; if ($this->domain !== null && $this->domain !== '') { $data['domain'] = $this->domain; } $reqs = AuthManager::singleton()->getAuthenticationRequests(AuthManager::ACTION_CREATE); $reqs = AuthenticationRequest::loadRequestsFromSubmission($reqs, $data); $res = AuthManager::singleton()->beginAccountCreation($wgUser, $reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: return true; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... $msg = $res->message instanceof \Message ? $res->message : new \Message($res->message); $this->logger->info(__METHOD__ . ': Authentication failed: ' . $msg->plain()); return false; default: throw new \BadMethodCallException('AuthManager does not support such simplified account creation'); } }
/** * Generates a form from the given request. * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name * @param string|Message $msg * @param string $msgType * @return HTMLForm */ protected function getAuthForm(array $requests, $action, $msg = '', $msgType = 'error') { global $wgSecureLogin, $wgLoginLanguageSelector; // FIXME merge this with parent if (isset($this->authForm)) { return $this->authForm; } $usingHTTPS = $this->getRequest()->getProtocol() === 'https'; // get basic form description from the auth logic $fieldInfo = AuthenticationRequest::mergeFieldInfo($requests); $fakeTemplate = $this->getFakeTemplate($msg, $msgType); $this->fakeTemplate = $fakeTemplate; // FIXME there should be a saner way to pass this to the hook // this will call onAuthChangeFormFields() $formDescriptor = static::fieldInfoToFormDescriptor($requests, $fieldInfo, $this->authAction); $this->postProcessFormDescriptor($formDescriptor, $requests); $context = $this->getContext(); if ($context->getRequest() !== $this->getRequest()) { // We have overridden the request, need to make sure the form uses that too. $context = new DerivativeContext($this->getContext()); $context->setRequest($this->getRequest()); } $form = HTMLForm::factory('vform', $formDescriptor, $context); $form->addHiddenField('authAction', $this->authAction); if ($wgLoginLanguageSelector) { $form->addHiddenField('uselang', $this->mLanguage); } $form->addHiddenField('force', $this->securityLevel); $form->addHiddenField($this->getTokenName(), $this->getToken()->toString()); if ($wgSecureLogin) { // If using HTTPS coming from HTTP, then the 'fromhttp' parameter must be preserved if (!$this->isSignup()) { $form->addHiddenField('wpForceHttps', (int) $this->mStickHTTPS); $form->addHiddenField('wpFromhttp', $usingHTTPS); } } // set properties of the form itself $form->setAction($this->getPageTitle()->getLocalURL($this->getReturnToQueryStringFragment())); $form->setName('userlogin' . ($this->isSignup() ? '2' : '')); if ($this->isSignup()) { $form->setId('userlogin2'); } $form->suppressDefaultSubmit(); $this->authForm = $form; return $form; }
/** * Check to see if the given clear-text password is one of the accepted passwords * @deprecated since 1.27, use AuthManager instead * @param string $password User password * @return bool True if the given password is correct, otherwise False */ public function checkPassword($password) { global $wgAuth, $wgLegacyEncoding, $wgDisableAuthManager; if ($wgDisableAuthManager) { $this->load(); // Some passwords will give a fatal Status, which means there is // some sort of technical or security reason for this password to // be completely invalid and should never be checked (e.g., T64685) if (!$this->checkPasswordValidity($password)->isOK()) { return false; } // Certain authentication plugins do NOT want to save // domain passwords in a mysql database, so we should // check this (in case $wgAuth->strict() is false). if ($wgAuth->authenticate($this->getName(), $password)) { return true; } elseif ($wgAuth->strict()) { // Auth plugin doesn't allow local authentication return false; } elseif ($wgAuth->strictUserAuth($this->getName())) { // Auth plugin doesn't allow local authentication for this user name return false; } $passwordFactory = new PasswordFactory(); $passwordFactory->init(RequestContext::getMain()->getConfig()); $db = $this->queryFlagsUsed & self::READ_LATEST ? wfGetDB(DB_MASTER) : wfGetDB(DB_SLAVE); try { $mPassword = $passwordFactory->newFromCiphertext($db->selectField('user', 'user_password', ['user_id' => $this->getId()], __METHOD__)); } catch (PasswordError $e) { wfDebug('Invalid password hash found in database.'); $mPassword = PasswordFactory::newInvalidPassword(); } if (!$mPassword->equals($password)) { if ($wgLegacyEncoding) { // Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted // Check for this with iconv $cp1252Password = iconv('UTF-8', 'WINDOWS-1252//TRANSLIT', $password); if ($cp1252Password === $password || !$mPassword->equals($cp1252Password)) { return false; } } else { return false; } } if ($passwordFactory->needsUpdate($mPassword) && !wfReadOnly()) { $this->setPasswordInternal($password); } return true; } else { $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN), ['username' => $this->getName(), 'password' => $password]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: return true; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $res->message->plain()); return false; default: throw new BadMethodCallException('AuthManager returned a response unsupported by ' . __METHOD__); } } }
/** * Executes the log-in attempt using the parameters passed. If * the log-in succeeds, it attaches a cookie to the session * and outputs the user id, username, and session token. If a * log-in fails, as the result of a bad password, a nonexistent * user, or any other reason, the host is cached with an expiry * and no log-in attempts will be accepted until that expiry * is reached. The expiry is $this->mLoginThrottle. */ public function execute() { // If we're in a mode that breaks the same-origin policy, no tokens can // be obtained if ($this->lacksSameOriginSecurity()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when the same-origin policy is not applied']); return; } $params = $this->extractRequestParams(); $result = []; // Make sure session is persisted $session = MediaWiki\Session\SessionManager::getGlobalSession(); $session->persist(); // Make sure it's possible to log in if (!$session->canSetUser()) { $this->getResult()->addValue(null, 'login', ['result' => 'Aborted', 'reason' => 'Cannot log in when using ' . $session->getProvider()->describe(Language::factory('en'))]); return; } $authRes = false; $context = new DerivativeContext($this->getContext()); $loginType = 'N/A'; // Check login token $token = $session->getToken('', 'login'); if ($token->wasNew() || !$params['token']) { $authRes = 'NeedToken'; } elseif (!$token->match($params['token'])) { $authRes = 'WrongToken'; } // Try bot passwords if ($authRes === false && $this->getConfig()->get('EnableBotPasswords') && strpos($params['name'], BotPassword::getSeparator()) !== false) { $status = BotPassword::login($params['name'], $params['password'], $this->getRequest()); if ($status->isOK()) { $session = $status->getValue(); $authRes = 'Success'; $loginType = 'BotPassword'; } else { $authRes = 'Failed'; $message = $status->getMessage(); LoggerFactory::getInstance('authmanager')->info('BotPassword login failed: ' . $status->getWikiText(false, false, 'en')); } } if ($authRes === false) { if ($this->getConfig()->get('DisableAuthManager')) { // Non-AuthManager login $context->setRequest(new DerivativeRequest($this->getContext()->getRequest(), ['wpName' => $params['name'], 'wpPassword' => $params['password'], 'wpDomain' => $params['domain'], 'wpLoginToken' => $params['token'], 'wpRemember' => ''])); $loginForm = new LoginForm(); $loginForm->setContext($context); $authRes = $loginForm->authenticateUserData(); $loginType = 'LoginForm'; switch ($authRes) { case LoginForm::SUCCESS: $authRes = 'Success'; break; case LoginForm::NEED_TOKEN: $authRes = 'NeedToken'; break; } } else { // Simplified AuthManager login, for backwards compatibility $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN, $this->getUser()), ['username' => $params['name'], 'password' => $params['password'], 'domain' => $params['domain'], 'rememberMe' => true]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: if ($this->getConfig()->get('EnableBotPasswords')) { $warn = 'Main-account login via action=login is deprecated and may stop working ' . 'without warning.'; $warn .= ' To continue login with action=login, see [[Special:BotPasswords]].'; $warn .= ' To safely continue using main-account login, see action=clientlogin.'; } else { $warn = 'Login via action=login is deprecated and may stop working without warning.'; $warn .= ' To safely log in, see action=clientlogin.'; } $this->setWarning($warn); $authRes = 'Success'; $loginType = 'AuthManager'; break; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... $authRes = 'Failed'; $message = $res->message; \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $message->plain()); break; default: $authRes = 'Aborted'; break; } } } $result['result'] = $authRes; switch ($authRes) { case 'Success': if ($this->getConfig()->get('DisableAuthManager')) { $user = $context->getUser(); $this->getContext()->setUser($user); $user->setCookies($this->getRequest(), null, true); } else { $user = $session->getUser(); } ApiQueryInfo::resetTokenCache(); // Deprecated hook $injected_html = ''; Hooks::run('UserLoginComplete', [&$user, &$injected_html]); $result['lguserid'] = intval($user->getId()); $result['lgusername'] = $user->getName(); // @todo: These are deprecated, and should be removed at some // point (1.28 at the earliest, and see T121527). They were ok // when the core cookie-based login was the only thing, but // CentralAuth broke that a while back and // SessionManager/AuthManager *really* break it. $result['lgtoken'] = $user->getToken(); $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case 'NeedToken': $result['token'] = $token->toString(); $this->setWarning('Fetching a token via action=login is deprecated. ' . 'Use action=query&meta=tokens&type=login instead.'); $this->logFeatureUsage('action=login&!lgtoken'); // @todo: See above about deprecation $result['cookieprefix'] = $this->getConfig()->get('CookiePrefix'); $result['sessionid'] = $session->getId(); break; case 'WrongToken': break; case 'Failed': $result['reason'] = $message->useDatabase('false')->inLanguage('en')->text(); break; case 'Aborted': $result['reason'] = 'Authentication requires user interaction, ' . 'which is not supported by action=login.'; if ($this->getConfig()->get('EnableBotPasswords')) { $result['reason'] .= ' To be able to login with action=login, see [[Special:BotPasswords]].'; $result['reason'] .= ' To continue using main-account login, see action=clientlogin.'; } else { $result['reason'] .= ' To log in, see action=clientlogin.'; } break; // Results from LoginForm for when $wgDisableAuthManager is true // Results from LoginForm for when $wgDisableAuthManager is true case LoginForm::WRONG_TOKEN: $result['result'] = 'WrongToken'; break; case LoginForm::NO_NAME: $result['result'] = 'NoName'; break; case LoginForm::ILLEGAL: $result['result'] = 'Illegal'; break; case LoginForm::WRONG_PLUGIN_PASS: $result['result'] = 'WrongPluginPass'; break; case LoginForm::NOT_EXISTS: $result['result'] = 'NotExists'; break; // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. // bug 20223 - Treat a temporary password as wrong. Per SpecialUserLogin: // The e-mailed temporary password should not be used for actual logins. case LoginForm::RESET_PASS: case LoginForm::WRONG_PASS: $result['result'] = 'WrongPass'; break; case LoginForm::EMPTY_PASS: $result['result'] = 'EmptyPass'; break; case LoginForm::CREATE_BLOCKED: $result['result'] = 'CreateBlocked'; $result['details'] = 'Your IP address is blocked from account creation'; $block = $context->getUser()->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::THROTTLED: $result['result'] = 'Throttled'; $result['wait'] = intval($loginForm->mThrottleWait); break; case LoginForm::USER_BLOCKED: $result['result'] = 'Blocked'; $block = User::newFromName($params['name'])->getBlock(); if ($block) { $result = array_merge($result, ApiQueryUserInfo::getBlockInfo($block)); } break; case LoginForm::ABORTED: $result['result'] = 'Aborted'; $result['reason'] = $loginForm->mAbortLoginErrorMsg; break; default: ApiBase::dieDebug(__METHOD__, "Unhandled case value: {$authRes}"); } $this->getResult()->addValue(null, 'login', $result); if ($loginType === 'LoginForm' && isset(LoginForm::$statusCodes[$authRes])) { $authRes = LoginForm::$statusCodes[$authRes]; } LoggerFactory::getInstance('authmanager')->info('Login attempt', ['event' => 'login', 'successful' => $authRes === 'Success', 'loginType' => $loginType, 'status' => $authRes]); }
public function setPassword($user, $password) { $data = ['username' => $user->getName(), 'password' => $password]; if ($this->domain !== null && $this->domain !== '') { $data['domain'] = $this->domain; } $reqs = AuthManager::singleton()->getAuthenticationRequests(AuthManager::ACTION_CHANGE); $reqs = AuthenticationRequest::loadRequestsFromSubmission($reqs, $data); foreach ($reqs as $req) { $status = AuthManager::singleton()->allowsAuthenticationDataChange($req); if (!$status->isGood()) { $this->logger->info(__METHOD__ . ': Password change rejected: {reason}', ['username' => $data['username'], 'reason' => $status->getWikiText(null, null, 'en')]); return false; } } foreach ($reqs as $req) { AuthManager::singleton()->changeAuthenticationData($req); } return true; }
/** * Generates a HTMLForm descriptor array from a set of authentication requests. * @param AuthenticationRequest[] $requests * @param string $action AuthManager action name (one of the AuthManager::ACTION_* constants) * @return array */ protected function getAuthFormDescriptor($requests, $action) { $fieldInfo = AuthenticationRequest::mergeFieldInfo($requests); $formDescriptor = $this->fieldInfoToFormDescriptor($requests, $fieldInfo, $action); $this->addTabIndex($formDescriptor); return $formDescriptor; }
public function getUniqueId() { return parent::getUniqueId() . ':' . $this->name; }
public function testForAuthentication(array $reqs) { if (!$this->passwordAttemptThrottle) { return \StatusValue::newGood(); } $ip = $this->manager->getRequest()->getIP(); try { $username = AuthenticationRequest::getUsernameFromRequests($reqs); } catch (\UnexpectedValueException $e) { $username = ''; } // Get everything this username could normalize to, and throttle each one individually. // If nothing uses usernames, just throttle by IP. $usernames = $this->manager->normalizeUsername($username); $result = false; foreach ($usernames as $name) { $r = $this->passwordAttemptThrottle->increase($name, $ip, __METHOD__); if ($r && (!$result || $result['wait'] < $r['wait'])) { $result = $r; } } if ($result) { $message = wfMessage('login-throttled')->durationParams($result['wait']); return \StatusValue::newFatal($message); } else { $this->manager->setAuthenticationSessionData('LoginThrottle', ['users' => $usernames, 'ip' => $ip]); return \StatusValue::newGood(); } }
/** * Continue the link attempt * @param User $user * @param string $key Session key to look in * @param AuthenticationRequest[] $reqs * @return AuthenticationResponse */ protected function continueLinkAttempt($user, $key, array $reqs) { $req = ButtonAuthenticationRequest::getRequestByName($reqs, 'linkOk'); if ($req) { return AuthenticationResponse::newPass(); } $req = AuthenticationRequest::getRequestByClass($reqs, ConfirmLinkAuthenticationRequest::class); if (!$req) { // WTF? Retry. return $this->beginLinkAttempt($user, $key); } $session = $this->manager->getRequest()->getSession(); $state = $session->getSecret($key); if (!is_array($state)) { return AuthenticationResponse::newAbstain(); } $maybeLink = []; foreach ($state['maybeLink'] as $linkReq) { $maybeLink[$linkReq->getUniqueId()] = $linkReq; } if (!$maybeLink) { return AuthenticationResponse::newAbstain(); } $state['maybeLink'] = []; $session->setSecret($key, $state); $statuses = []; $anyFailed = false; foreach ($req->confirmedLinkIDs as $id) { if (isset($maybeLink[$id])) { $req = $maybeLink[$id]; $req->username = $user->getName(); if (!$req->action) { // Make sure the action is set, but don't override it if // the provider filled it in. $req->action = AuthManager::ACTION_CHANGE; } $status = $this->manager->allowsAuthenticationDataChange($req); $statuses[] = [$req, $status]; if ($status->isGood()) { $this->manager->changeAuthenticationData($req); } else { $anyFailed = true; } } } if (!$anyFailed) { return AuthenticationResponse::newPass(); } $combinedStatus = \Status::newGood(); foreach ($statuses as $data) { list($req, $status) = $data; $descriptionInfo = $req->describeCredentials(); $description = wfMessage('authprovider-confirmlink-option', $descriptionInfo['provider']->text(), $descriptionInfo['account']->text())->text(); if ($status->isGood()) { $combinedStatus->error(wfMessage('authprovider-confirmlink-success-line', $description)); } else { $combinedStatus->error(wfMessage('authprovider-confirmlink-failure-line', $description, $status->getMessage()->text())); } } return AuthenticationResponse::newUI([new ButtonAuthenticationRequest('linkOk', wfMessage('ok'), wfMessage('authprovider-confirmlink-ok-help'))], $combinedStatus->getMessage('authprovider-confirmlink-failed')); }
/** * Check to see if the given clear-text password is one of the accepted passwords * @deprecated since 1.27, use AuthManager instead * @param string $password User password * @return bool True if the given password is correct, otherwise False */ public function checkPassword($password) { $manager = AuthManager::singleton(); $reqs = AuthenticationRequest::loadRequestsFromSubmission($manager->getAuthenticationRequests(AuthManager::ACTION_LOGIN), ['username' => $this->getName(), 'password' => $password]); $res = AuthManager::singleton()->beginAuthentication($reqs, 'null:'); switch ($res->status) { case AuthenticationResponse::PASS: return true; case AuthenticationResponse::FAIL: // Hope it's not a PreAuthenticationProvider that failed... \MediaWiki\Logger\LoggerFactory::getInstance('authentication')->info(__METHOD__ . ': Authentication failed: ' . $res->message->plain()); return false; default: throw new BadMethodCallException('AuthManager returned a response unsupported by ' . __METHOD__); } }