Page MenuHomePhabricator

D9252.id.diff
No OneTemporary

D9252.id.diff

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<token>\w+)/' => 'PhabricatorEmailTokenController',
+ 'once/'.
+ '(?P<type>[^/]+)/'.
+ '(?P<id>\d+)/'.
+ '(?P<key>[^/]+)/'.
+ '(?:(?P<emailID>\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 @@
+<?php
+
+final class PhabricatorAuthOneTimeLoginController
+ extends PhabricatorAuthController {
+
+ private $id;
+ private $key;
+ private $emailID;
+ private $linkType;
+
+ public function shouldRequireLogin() {
+ return false;
+ }
+
+ public function willProcessRequest(array $data) {
+ $this->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 = <<<EOBODY
You can use this link to reset your Phabricator password:
@@ -87,24 +92,18 @@
// mail if they have the "don't send me email about my own actions"
// preference set.
- $mail = new PhabricatorMetaMTAMail();
- $mail->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 @@
-<?php
-
-final class PhabricatorEmailTokenController
- extends PhabricatorAuthController {
-
- private $token;
-
- public function shouldRequireLogin() {
- return false;
- }
-
- public function willProcessRequest(array $data) {
- $this->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 = <<<EOBODY
Welcome to Phabricator!
@@ -611,7 +562,11 @@
$password_instructions = null;
if (PhabricatorAuthProviderPassword::getPasswordProvider()) {
- $uri = $this->getEmailLoginURI();
+ $engine = new PhabricatorAuthSessionEngine();
+ $uri = $engine->getOneTimeLoginURI(
+ $this,
+ null,
+ PhabricatorAuthSessionEngine::ONETIME_USERNAME);
$password_instructions = <<<EOTXT
If you use a password to login, you'll need to reset it before you can login
again. You can reset your password by following this link:
diff --git a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
--- a/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
+++ b/src/applications/settings/panel/PhabricatorSettingsPanelPassword.php
@@ -47,17 +47,17 @@
// either by providing the old password or by carrying a token to
// the workflow from a password reset email.
- $token = $request->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'))

File Metadata

Mime Type
text/plain
Expires
Jul 31 2025, 10:44 AM (8 w, 6 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/m5/re/atceacse4enoywvo
Default Alt Text
D9252.id.diff (31 KB)

Event Timeline