Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F18945152
D9252.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
31 KB
Referenced Files
None
Subscribers
None
D9252.diff
View Options
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
Details
Attached
Mime Type
text/plain
Expires
Nov 12 2025, 5:47 PM (9 w, 3 h ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/m5/re/atceacse4enoywvo
Default Alt Text
D9252.diff (31 KB)
Attached To
Mode
D9252: Make password reset emails use one-time tokens
Attached
Detach File
Event Timeline
Log In to Comment