diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1249,6 +1249,7 @@ 'PhabricatorAuthNeedsApprovalController' => 'applications/auth/controller/PhabricatorAuthNeedsApprovalController.php', 'PhabricatorAuthNewController' => 'applications/auth/controller/config/PhabricatorAuthNewController.php', 'PhabricatorAuthOldOAuthRedirectController' => 'applications/auth/controller/PhabricatorAuthOldOAuthRedirectController.php', + 'PhabricatorAuthOneTimeLoginController' => 'applications/auth/controller/PhabricatorAuthOneTimeLoginController.php', 'PhabricatorAuthPHIDTypeAuthFactor' => 'applications/auth/phid/PhabricatorAuthPHIDTypeAuthFactor.php', 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 'PhabricatorAuthProviderConfig' => 'applications/auth/storage/PhabricatorAuthProviderConfig.php', @@ -1525,7 +1526,6 @@ 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', - 'PhabricatorEmailTokenController' => 'applications/auth/controller/PhabricatorEmailTokenController.php', 'PhabricatorEmailVerificationController' => 'applications/auth/controller/PhabricatorEmailVerificationController.php', 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php', 'PhabricatorEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorEnglishTranslation.php', @@ -4011,6 +4011,7 @@ 'PhabricatorAuthNeedsApprovalController' => 'PhabricatorAuthController', 'PhabricatorAuthNewController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthOldOAuthRedirectController' => 'PhabricatorAuthController', + 'PhabricatorAuthOneTimeLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthPHIDTypeAuthFactor' => 'PhabricatorPHIDType', 'PhabricatorAuthProviderConfig' => array( @@ -4325,7 +4326,6 @@ 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditor' => 'Phobject', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', - 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 'PhabricatorEmailVerificationController' => 'PhabricatorAuthController', 'PhabricatorEmptyQueryException' => 'Exception', 'PhabricatorEnglishTranslation' => 'PhabricatorBaseEnglishTranslation', diff --git a/src/applications/auth/application/PhabricatorApplicationAuth.php b/src/applications/auth/application/PhabricatorApplicationAuth.php --- a/src/applications/auth/application/PhabricatorApplicationAuth.php +++ b/src/applications/auth/application/PhabricatorApplicationAuth.php @@ -101,7 +101,11 @@ '/login/' => array( '' => 'PhabricatorAuthStartController', 'email/' => 'PhabricatorEmailLoginController', - 'etoken/(?P\w+)/' => 'PhabricatorEmailTokenController', + 'once/'. + '(?P[^/]+)/'. + '(?P\d+)/'. + '(?P[^/]+)/'. + '(?:(?P\d+)/)?' => 'PhabricatorAuthOneTimeLoginController', 'refresh/' => 'PhabricatorRefreshCSRFController', 'mustverify/' => 'PhabricatorMustVerifyEmailController', ), diff --git a/src/applications/auth/constants/PhabricatorCookies.php b/src/applications/auth/constants/PhabricatorCookies.php --- a/src/applications/auth/constants/PhabricatorCookies.php +++ b/src/applications/auth/constants/PhabricatorCookies.php @@ -49,6 +49,14 @@ const COOKIE_NEXTURI = 'next_uri'; + /** + * Stores a hint that the user should be moved directly into high security + * after upgrading a partial login session. This is used during password + * recovery to avoid a double-prompt. + */ + const COOKIE_HISEC = 'jump_to_hisec'; + + /* -( Client ID Cookie )--------------------------------------------------- */ @@ -125,7 +133,7 @@ */ public static function getNextURICookie(AphrontRequest $request) { $cookie_value = $request->getCookie(self::COOKIE_NEXTURI); - list($set_at, $next_uri) = self::parseNExtURICookie($cookie_value); + list($set_at, $next_uri) = self::parseNextURICookie($cookie_value); return $next_uri; } diff --git a/src/applications/auth/controller/PhabricatorAuthFinishController.php b/src/applications/auth/controller/PhabricatorAuthFinishController.php --- a/src/applications/auth/controller/PhabricatorAuthFinishController.php +++ b/src/applications/auth/controller/PhabricatorAuthFinishController.php @@ -24,11 +24,19 @@ $engine = new PhabricatorAuthSessionEngine(); + // If this cookie is set, the user is headed into a high security area + // after login (normally because of a password reset) so if they are + // able to pass the checkpoint we just want to put their account directly + // into high security mode, rather than prompt them again for the same + // set of credentials. + $jump_into_hisec = $request->getCookie(PhabricatorCookies::COOKIE_HISEC); + try { $token = $engine->requireHighSecuritySession( $viewer, $request, - '/logout/'); + '/logout/', + $jump_into_hisec); } catch (PhabricatorAuthHighSecurityRequiredException $ex) { $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $ex->getFactors(), @@ -60,6 +68,7 @@ $next = PhabricatorCookies::getNextURICookie($request); $request->clearCookie(PhabricatorCookies::COOKIE_NEXTURI); + $request->clearCookie(PhabricatorCookies::COOKIE_HISEC); if (!PhabricatorEnv::isValidLocalWebResource($next)) { $next = '/'; diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -0,0 +1,199 @@ +linkType = $data['type']; + $this->id = $data['id']; + $this->key = $data['key']; + $this->emailID = idx($data, 'emailID'); + } + + public function processRequest() { + $request = $this->getRequest(); + + if ($request->getUser()->isLoggedIn()) { + return $this->renderError( + pht('You are already logged in.')); + } + + $target_user = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->id)) + ->executeOne(); + if (!$target_user) { + return new Aphront404Response(); + } + + // NOTE: As a convenience to users, these one-time login URIs may also + // be associated with an email address which will be verified when the + // URI is used. + + // This improves the new user experience for users receiving "Welcome" + // emails on installs that require verification: if we did not verify the + // email, they'd immediately get roadblocked with a "Verify Your Email" + // error and have to go back to their email account, wait for a + // "Verification" email, and then click that link to actually get access to + // their account. This is hugely unwieldy, and if the link was only sent + // to the user's email in the first place we can safely verify it as a + // side effect of login. + + // The email hashed into the URI so users can't verify some email they + // do not own by doing this: + // + // - Add some address you do not own; + // - request a password reset; + // - change the URI in the email to the address you don't own; + // - login via the email link; and + // - get a "verified" address you don't control. + + $target_email = null; + if ($this->emailID) { + $target_email = id(new PhabricatorUserEmail())->loadOneWhere( + 'userPHID = %s AND id = %d', + $target_user->getPHID(), + $this->emailID); + if (!$target_email) { + return new Aphront404Response(); + } + } + + $engine = new PhabricatorAuthSessionEngine(); + $token = $engine->loadOneTimeLoginKey( + $target_user, + $target_email, + $this->key); + + if (!$token) { + return $this->newDialog() + ->setTitle(pht('Unable to Login')) + ->setShortTitle(pht('Login Failure')) + ->appendParagraph( + pht( + 'The login link you clicked is invalid, out of date, or has '. + 'already been used.')) + ->appendParagraph( + pht( + 'Make sure you are copy-and-pasting the entire link into '. + 'your browser. Login links are only valid for 24 hours, and '. + 'can only be used once.')) + ->appendParagraph( + pht('You can try again, or request a new link via email.')) + ->addCancelButton('/login/email/', pht('Send Another Email')); + } + + if ($request->isFormPost()) { + // If we have an email bound into this URI, verify email so that clicking + // the link in the "Welcome" email is good enough, without requiring users + // to go through a second round of email verification. + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + // Nuke the token so that this URI is one-time only. + $token->delete(); + + if ($target_email) { + $target_user->openTransaction(); + $target_email->setIsVerified(1); + $target_email->save(); + + // If this was the user's primary email address, also mark their + // account as verified. + $primary_email = $target_user->loadPrimaryEmail(); + if ($primary_email->getID() == $target_email->getID()) { + $target_user->setIsEmailVerified(1); + $target_user->save(); + } + $target_user->saveTransaction(); + } + unset($unguarded); + + $next = '/'; + if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { + $next = '/settings/panel/external/'; + } else if (PhabricatorEnv::getEnvConfig('account.editable')) { + + // We're going to let the user reset their password without knowing + // the old one. Generate a one-time token for that. + $key = Filesystem::readRandomCharacters(16); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + id(new PhabricatorAuthTemporaryToken()) + ->setObjectPHID($target_user->getPHID()) + ->setTokenType( + PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE) + ->setTokenExpires(time() + phutil_units('1 hour in seconds')) + ->setTokenCode(PhabricatorHash::digest($key)) + ->save(); + unset($unguarded); + + $next = (string)id(new PhutilURI('/settings/panel/password/')) + ->setQueryParams( + array( + 'key' => $key, + )); + + $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); + } + + PhabricatorCookies::setNextURICookie($request, $next, $force = true); + + return $this->loginUser($target_user); + } + + // NOTE: We need to CSRF here so attackers can't generate an email link, + // then log a user in to an account they control via sneaky invisible + // form submissions. + + switch ($this->linkType) { + case PhabricatorAuthSessionEngine::ONETIME_WELCOME: + $title = pht('Welcome to Phabricator'); + break; + case PhabricatorAuthSessionEngine::ONETIME_RECOVER: + $title = pht('Account Recovery'); + break; + case PhabricatorAuthSessionEngine::ONETIME_USERNAME: + case PhabricatorAuthSessionEngine::ONETIME_RESET: + default: + $title = pht('Login to Phabricator'); + break; + } + + $body = array(); + $body[] = pht( + 'Use the button below to log in as: %s', + phutil_tag('strong', array(), $target_user->getUsername())); + + if ($target_email && !$target_email->getIsVerified()) { + $body[] = pht( + 'Logging in will verify %s as an email address you own.', + phutil_tag('strong', array(), $target_email->getAddress())); + + } + + $body[] = pht( + 'After logging in you should set a password for your account, or '. + 'link your account to an external account that you can use to '. + 'authenticate in the future.'); + + $dialog = $this->newDialog() + ->setTitle($title) + ->addSubmitButton(pht('Login (%s)', $target_user->getUsername())) + ->addCancelButton('/'); + + foreach ($body as $paragraph) { + $dialog->appendParagraph($paragraph); + } + + return id(new AphrontDialogResponse())->setDialog($dialog); + } +} diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -59,7 +59,12 @@ } if (!$errors) { - $uri = $target_user->getEmailLoginURI($target_email); + $engine = new PhabricatorAuthSessionEngine(); + $uri = $engine->getOneTimeLoginURI( + $target_user, + null, + PhabricatorAuthSessionEngine::ONETIME_RESET); + if ($is_serious) { $body = <<setSubject('[Phabricator] Password Reset'); - $mail->addTos( - array( - $target_user->getPHID(), - )); - $mail->setBody($body); - $mail->saveAndSend(); - - $view = new AphrontRequestFailureView(); - $view->setHeader(pht('Check Your Email')); - $view->appendChild(phutil_tag('p', array(), pht( - 'An email has been sent with a link you can use to login.'))); - return $this->buildStandardPageResponse( - $view, - array( - 'title' => pht('Email Sent'), - )); + $mail = id(new PhabricatorMetaMTAMail()) + ->setSubject(pht('[Phabricator] Password Reset')) + ->addRawTos(array($target_email->getAddress())) + ->setBody($body) + ->saveAndSend(); + + return $this->newDialog() + ->setTitle(pht('Check Your Email')) + ->setShortTitle(pht('Email Sent')) + ->appendParagraph( + pht('An email has been sent with a link you can use to login.')) + ->addCancelButton('/', pht('Done')); } } diff --git a/src/applications/auth/controller/PhabricatorEmailTokenController.php b/src/applications/auth/controller/PhabricatorEmailTokenController.php deleted file mode 100644 --- a/src/applications/auth/controller/PhabricatorEmailTokenController.php +++ /dev/null @@ -1,128 +0,0 @@ -token = $data['token']; - } - - public function processRequest() { - $request = $this->getRequest(); - - if ($request->getUser()->isLoggedIn()) { - return $this->renderError( - pht('You are already logged in.')); - } - - $token = $this->token; - $email = $request->getStr('email'); - - $target_email = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email); - - $target_user = null; - if ($target_email) { - $target_user = id(new PhabricatorUser())->loadOneWhere( - 'phid = %s', - $target_email->getUserPHID()); - } - - // NOTE: We need to bind verification to **addresses**, not **users**, - // because we verify addresses when they're used to login this way, and if - // we have a user-based verification you can: - // - // - Add some address you do not own; - // - request a password reset; - // - change the URI in the email to the address you don't own; - // - login via the email link; and - // - get a "verified" address you don't control. - - if (!$target_email || - !$target_user || - !$target_user->validateEmailToken($target_email, $token)) { - - $view = new AphrontRequestFailureView(); - $view->setHeader(pht('Unable to Login')); - $view->appendChild(phutil_tag('p', array(), pht( - 'The authentication information in the link you clicked is '. - 'invalid or out of date. Make sure you are copy-and-pasting the '. - 'entire link into your browser. You can try again, or request '. - 'a new email.'))); - $view->appendChild(phutil_tag_div( - 'aphront-failure-continue', - phutil_tag( - 'a', - array('class' => 'button', 'href' => '/login/email/'), - pht('Send Another Email')))); - - return $this->buildStandardPageResponse( - $view, - array( - 'title' => pht('Login Failure'), - )); - } - - if ($request->isFormPost()) { - // Verify email so that clicking the link in the "Welcome" email is good - // enough, without requiring users to go through a second round of email - // verification. - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - $target_email->setIsVerified(1); - $target_email->save(); - unset($unguarded); - - $next = '/'; - if (!PhabricatorAuthProviderPassword::getPasswordProvider()) { - $next = '/settings/panel/external/'; - } else if (PhabricatorEnv::getEnvConfig('account.editable')) { - $next = (string)id(new PhutilURI('/settings/panel/password/')) - ->setQueryParams( - array( - 'token' => $token, - 'email' => $email, - )); - } - - PhabricatorCookies::setNextURICookie($request, $next, $force = true); - - return $this->loginUser($target_user); - } - - // NOTE: We need to CSRF here so attackers can't generate an email link, - // then log a user in to an account they control via sneaky invisible - // form submissions. - - // TODO: Since users can arrive here either through password reset or - // through welcome emails, it might be nice to include the workflow type - // in the URI or query params so we can tailor the messaging. Right now, - // it has to be generic enough to make sense in either workflow, which - // leaves it feeling a little awkward. - - $dialog = id(new AphrontDialogView()) - ->setUser($request->getUser()) - ->setTitle(pht('Login to Phabricator')) - ->addHiddenInput('email', $email) - ->appendParagraph( - pht( - 'Use the button below to log in as: %s', - phutil_tag('strong', array(), $email))) - ->appendParagraph( - pht( - 'After logging in you should set a password for your account, or '. - 'link your account to an external account that you can use to '. - 'authenticate in the future.')) - ->addSubmitButton(pht('Login (%s)', $email)) - ->addCancelButton('/'); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } -} diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -6,6 +6,7 @@ * @task new Creating Sessions * @task hisec High Security * @task partial Partial Sessions + * @task onetime One Time Login URIs */ final class PhabricatorAuthSessionEngine extends Phobject { @@ -39,6 +40,23 @@ /** + * Temporary tokens for one time logins. + */ + const ONETIME_TEMPORARY_TOKEN_TYPE = 'login:onetime'; + + + /** + * Temporary tokens for password recovery after one time login. + */ + const PASSWORD_TEMPORARY_TOKEN_TYPE = 'login:password'; + + const ONETIME_RECOVER = 'recover'; + const ONETIME_RESET = 'reset'; + const ONETIME_WELCOME = 'welcome'; + const ONETIME_USERNAME = 'rename'; + + + /** * Get the session kind (e.g., anonymous, user, external account) from a * session token. Returns a `KIND_` constant. * @@ -245,13 +263,17 @@ * @param PhabricatorUser User whose session needs to be in high security. * @param AphrontReqeust Current request. * @param string URI to return the user to if they cancel. + * @param bool True to jump partial sessions directly into high + * security instead of just upgrading them to full + * sessions. * @return PhabricatorAuthHighSecurityToken Security token. * @task hisec */ public function requireHighSecuritySession( PhabricatorUser $viewer, AphrontRequest $request, - $cancel_uri) { + $cancel_uri, + $jump_into_hisec = false) { if (!$viewer->hasSession()) { throw new Exception( @@ -320,9 +342,10 @@ new PhabricatorAuthTryFactorAction(), -1); - if ($session->getIsPartial()) { - // If we have a partial session, just issue a token without - // putting it in high security mode. + if ($session->getIsPartial() && !$jump_into_hisec) { + // If we have a partial session and are not jumping directly into + // hisec, just issue a token without putting it in high security + // mode. return $this->issueHighSecurityToken($session, true); } @@ -459,6 +482,7 @@ * @task partial */ public function upgradePartialSession(PhabricatorUser $viewer) { + if (!$viewer->hasSession()) { throw new Exception( pht('Upgrading partial session of user with no session!')); @@ -486,7 +510,112 @@ PhabricatorUserLog::ACTION_LOGIN_FULL); $log->save(); unset($unguarded); + } + + +/* -( One Time Login URIs )------------------------------------------------ */ + + + /** + * Retrieve a temporary, one-time URI which can log in to an account. + * + * These URIs are used for password recovery and to regain access to accounts + * which users have been locked out of. + * + * @param PhabricatorUser User to generate a URI for. + * @param PhabricatorUserEmail Optionally, email to verify when + * link is used. + * @param string Optional context string for the URI. This is purely cosmetic + * and used only to customize workflow and error messages. + * @return string Login URI. + * @task onetime + */ + public function getOneTimeLoginURI( + PhabricatorUser $user, + PhabricatorUserEmail $email = null, + $type = self::ONETIME_RESET) { + + $key = Filesystem::readRandomCharacters(32); + $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + id(new PhabricatorAuthTemporaryToken()) + ->setObjectPHID($user->getPHID()) + ->setTokenType(self::ONETIME_TEMPORARY_TOKEN_TYPE) + ->setTokenExpires(time() + phutil_units('1 day in seconds')) + ->setTokenCode($key_hash) + ->save(); + unset($unguarded); + + $uri = '/login/once/'.$type.'/'.$user->getID().'/'.$key.'/'; + if ($email) { + $uri = $uri.$email->getID().'/'; + } + + try { + $uri = PhabricatorEnv::getProductionURI($uri); + } catch (Exception $ex) { + // If a user runs `bin/auth recover` before configuring the base URI, + // just show the path. We don't have any way to figure out the domain. + // See T4132. + } + + return $uri; + } + + + /** + * Load the temporary token associated with a given one-time login key. + * + * @param PhabricatorUser User to load the token for. + * @param PhabricatorUserEmail Optionally, email to verify when + * link is used. + * @param string Key user is presenting as a valid one-time login key. + * @return PhabricatorAuthTemporaryToken|null Token, if one exists. + * @task onetime + */ + public function loadOneTimeLoginKey( + PhabricatorUser $user, + PhabricatorUserEmail $email = null, + $key = null) { + + $key_hash = $this->getOneTimeLoginKeyHash($user, $email, $key); + + return id(new PhabricatorAuthTemporaryTokenQuery()) + ->setViewer($user) + ->withObjectPHIDs(array($user->getPHID())) + ->withTokenTypes(array(self::ONETIME_TEMPORARY_TOKEN_TYPE)) + ->withTokenCodes(array($key_hash)) + ->withExpired(false) + ->executeOne(); + } + + + /** + * Hash a one-time login key for storage as a temporary token. + * + * @param PhabricatorUser User this key is for. + * @param PhabricatorUserEmail Optionally, email to verify when + * link is used. + * @param string The one time login key. + * @return string Hash of the key. + * task onetime + */ + private function getOneTimeLoginKeyHash( + PhabricatorUser $user, + PhabricatorUserEmail $email = null, + $key = null) { + + $parts = array( + $key, + $user->getAccountSecret(), + ); + + if ($email) { + $parts[] = $email->getVerificationCode(); + } + return PhabricatorHash::digest(implode(':', $parts)); } } diff --git a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php --- a/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php +++ b/src/applications/auth/management/PhabricatorAuthManagementRecoverWorkflow.php @@ -69,6 +69,12 @@ $can_recover)); } + $engine = new PhabricatorAuthSessionEngine(); + $onetime_uri = $engine->getOneTimeLoginURI( + $user, + null, + PhabricatorAuthSessionEngine::ONETIME_RECOVER); + $console = PhutilConsole::getConsole(); $console->writeOut( pht( @@ -76,7 +82,7 @@ 'interface:', $username)); $console->writeOut("\n\n"); - $console->writeOut(" %s", $user->getEmailLoginURI()); + $console->writeOut(' %s', $onetime_uri); $console->writeOut("\n\n"); $console->writeOut( pht( diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -342,60 +342,6 @@ return substr(PhabricatorHash::digest($vec), 0, $len); } - private function generateEmailToken( - PhabricatorUserEmail $email, - $offset = 0) { - - $key = implode( - '-', - array( - PhabricatorEnv::getEnvConfig('phabricator.csrf-key'), - $this->getPHID(), - $email->getVerificationCode(), - )); - - return $this->generateToken( - time() + ($offset * self::EMAIL_CYCLE_FREQUENCY), - self::EMAIL_CYCLE_FREQUENCY, - $key, - self::EMAIL_TOKEN_LENGTH); - } - - public function validateEmailToken( - PhabricatorUserEmail $email, - $token) { - for ($ii = -1; $ii <= 1; $ii++) { - $valid = $this->generateEmailToken($email, $ii); - if ($token == $valid) { - return true; - } - } - return false; - } - - public function getEmailLoginURI(PhabricatorUserEmail $email = null) { - if (!$email) { - $email = $this->loadPrimaryEmail(); - if (!$email) { - throw new Exception("User has no primary email!"); - } - } - $token = $this->generateEmailToken($email); - - $uri = '/login/etoken/'.$token.'/'; - try { - $uri = PhabricatorEnv::getProductionURI($uri); - } catch (Exception $ex) { - // If a user runs `bin/auth recover` before configuring the base URI, - // just show the path. We don't have any way to figure out the domain. - // See T4132. - } - - $uri = new PhutilURI($uri); - - return $uri->alter('email', $email->getAddress()); - } - public function attachUserProfile(PhabricatorUserProfile $profile) { $this->profile = $profile; return $this; @@ -567,7 +513,12 @@ $base_uri = PhabricatorEnv::getProductionURI('/'); - $uri = $this->getEmailLoginURI(); + $engine = new PhabricatorAuthSessionEngine(); + $uri = $engine->getOneTimeLoginURI( + $this, + $this->loadPrimaryEmail(), + PhabricatorAuthSessionEngine::ONETIME_WELCOME); + $body = <<getEmailLoginURI(); + $engine = new PhabricatorAuthSessionEngine(); + $uri = $engine->getOneTimeLoginURI( + $this, + null, + PhabricatorAuthSessionEngine::ONETIME_USERNAME); $password_instructions = <<getStr('token'); - - $valid_token = false; - if ($token) { - $email_address = $request->getStr('email'); - $email = id(new PhabricatorUserEmail())->loadOneWhere( - 'address = %s', - $email_address); - if ($email) { - $valid_token = $user->validateEmailToken($email, $token); - } + $key = $request->getStr('key'); + $token = null; + if ($key) { + $token = id(new PhabricatorAuthTemporaryTokenQuery()) + ->setViewer($user) + ->withObjectPHIDs(array($user->getPHID())) + ->withTokenTypes( + array(PhabricatorAuthSessionEngine::PASSWORD_TEMPORARY_TOKEN_TYPE)) + ->withTokenCodes(array(PhabricatorHash::digest($key))) + ->withExpired(false) + ->executeOne(); } $e_old = true; @@ -66,7 +66,7 @@ $errors = array(); if ($request->isFormPost()) { - if (!$valid_token) { + if (!$token) { $envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw')); if (!$user->comparePassword($envelope)) { $errors[] = pht('The old password you entered is incorrect.'); @@ -105,7 +105,10 @@ unset($unguarded); - if ($valid_token) { + if ($token) { + // Destroy the token. + $token->delete(); + // If this is a password set/reset, kick the user to the home page // after we update their account. $next = '/'; @@ -135,9 +138,9 @@ $form = new AphrontFormView(); $form ->setUser($user) - ->addHiddenInput('token', $token); + ->addHiddenInput('key', $key); - if (!$valid_token) { + if (!$token) { $form->appendChild( id(new AphrontFormPasswordControl()) ->setLabel(pht('Old Password'))