1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 16:52:41 +01:00

Separate "Set/Reset Password" from "Change Password"

Summary:
See PHI223. Ref T13024. There's a remaining registration/login order issue after the other changes in T13024: we lose track of the current URI when we go through the MFA flow, so we can lose "Set Password" at the end of the flow.

Specifically, the flow goes like this today:

  - User clicks the welcome link in email.
  - They get redirected to the "set password" settings panel.
  - This gets pre-empted by Legalpad (although we'll potentially survive this with the URI intact).
  - This also gets pre-empted by the "Set MFA" workflow. If the user completes this flow, they get redirected to a `/auth/multifactor/?id=123` sort of URI to highlight the factor they added. This causes us to lose the `/settings/panel/password/blah/blah?key=xyz` URI.

The ordering on this is also not ideal; it's preferable to start with a password, then do the other steps, so the user can return to the flow more easily if they are interrupted.

Resolve this by separating the "change your password" and "set/reset your password" flows onto two different pages. This copy/pastes a bit of code, but both flows end up simpler so it feels reasonable to me overall.

We don't require a full session for "set/reset password" (so you can do it if you don't have MFA/legalpad yet) and do it first.

This works better and is broadly simpler for users.

Test Plan:
  - Required MFA + legalpad, invited a user via email, registered.
    - Before: password set flow got lost when setting MFA.
    - After: prompted to set password, then sign documents, then set up MFA.
  - Reset password (with MFA confgiured, was required to MFA first).
  - Tried to reset password without a valid reset key, wasn't successful.
  - Changed password using existing flow.
  - Hit various (all?) error cases (short password, common password, mismatch, missing password, etc).

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13024

Differential Revision: https://secure.phabricator.com/D18840
This commit is contained in:
epriestley 2017-12-22 11:55:39 -08:00
parent 66b74073be
commit ad4db9b2f3
6 changed files with 185 additions and 59 deletions

View file

@ -2112,6 +2112,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
@ -7377,6 +7378,7 @@ phutil_register_library_map(array(
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
'PhabricatorAuthSessionInfo' => 'Phobject',
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',

View file

@ -84,6 +84,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
=> 'PhabricatorAuthSSHKeyDeactivateController',
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
),
'password/' => 'PhabricatorAuthSetPasswordController',
),
'/oauth/(?P<provider>\w+)/login/'

View file

@ -139,8 +139,7 @@ final class PhabricatorAuthOneTimeLoginController
->save();
unset($unguarded);
$username = $target_user->getUsername();
$panel_uri = "/settings/user/{$username}/page/password/";
$panel_uri = '/auth/password/';
$next = (string)id(new PhutilURI($panel_uri))
->setQueryParams(

View file

@ -0,0 +1,155 @@
<?php
final class PhabricatorAuthSetPasswordController
extends PhabricatorAuthController {
public function shouldAllowPartialSessions() {
return true;
}
public function shouldAllowLegallyNonCompliantUsers() {
return true;
}
public function handleRequest(AphrontRequest $request) {
$viewer = $this->getViewer();
if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) {
return new Aphront404Response();
}
$token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession(
$viewer,
$request,
'/');
$key = $request->getStr('key');
$password_type = PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
if (!$key) {
return new Aphront404Response();
}
$auth_token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($viewer)
->withTokenResources(array($viewer->getPHID()))
->withTokenTypes(array($password_type))
->withTokenCodes(array(PhabricatorHash::weakDigest($key)))
->withExpired(false)
->executeOne();
if (!$auth_token) {
return new Aphront404Response();
}
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
$e_password = true;
$e_confirm = true;
$errors = array();
if ($request->isFormPost()) {
$password = $request->getStr('password');
$confirm = $request->getStr('confirm');
$e_password = null;
$e_confirm = null;
if (!strlen($password)) {
$errors[] = pht('You must choose a password or skip this step.');
$e_password = pht('Required');
} else if (strlen($password) < $min_len) {
$errors[] = pht(
'The selected password is too short. Passwords must be a minimum '.
'of %s characters.',
new PhutilNumber($min_len));
$e_password = pht('Too Short');
} else if (!strlen($confirm)) {
$errors[] = pht('You must confirm the selecetd password.');
$e_confirm = pht('Required');
} else if ($password !== $confirm) {
$errors[] = pht('The password and confirmation do not match.');
$e_password = pht('Invalid');
$e_confirm = pht('Invalid');
} else if (PhabricatorCommonPasswords::isCommonPassword($password)) {
$e_password = pht('Very Weak');
$errors[] = pht(
'The selected password is very weak: it is one of the most common '.
'passwords in use. Choose a stronger password.');
}
if (!$errors) {
$envelope = new PhutilOpaqueEnvelope($password);
// This write is unguarded because the CSRF token has already
// been checked in the call to $request->isFormPost() and
// the CSRF token depends on the password hash, so when it
// is changed here the CSRF token check will fail.
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
id(new PhabricatorUserEditor())
->setActor($viewer)
->changePassword($viewer, $envelope);
unset($unguarded);
// Destroy the token.
$auth_token->delete();
return id(new AphrontRedirectResponse())->setURI('/');
}
}
$len_caption = null;
if ($min_len) {
$len_caption = pht('Minimum password length: %d characters.', $min_len);
}
if ($viewer->hasPassword()) {
$title = pht('Reset Password');
$crumb = pht('Reset Password');
$submit = pht('Reset Password');
} else {
$title = pht('Set Password');
$crumb = pht('Set Password');
$submit = pht('Set Account Password');
}
$form = id(new AphrontFormView())
->setViewer($viewer)
->addHiddenInput('key', $key)
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('New Password'))
->setError($e_password)
->setName('password'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)
->setLabel(pht('Confirm Password'))
->setCaption($len_caption)
->setError($e_confirm)
->setName('confirm'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/', pht('Skip This Step'))
->setValue($submit));
$form_box = id(new PHUIObjectBoxView())
->setHeaderText($title)
->setFormErrors($errors)
->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
->setForm($form);
$main_view = id(new PHUITwoColumnView())
->setFooter($form_box);
$crumbs = $this->buildApplicationCrumbs()
->addTextCrumb($crumb)
->setBorder(true);
return $this->newPage()
->setTitle($title)
->setCrumbs($crumbs)
->appendChild($main_view);
}
}

View file

@ -262,6 +262,10 @@ final class PhabricatorUser
PhabricatorPeopleUserPHIDType::TYPECONST);
}
public function hasPassword() {
return (bool)strlen($this->passwordHash);
}
public function setPassword(PhutilOpaqueEnvelope $envelope) {
if (!$this->getPHID()) {
throw new Exception(

View file

@ -35,23 +35,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
// NOTE: To change your password, you need to prove you own the account,
// either by providing the old password or by carrying a token to
// the workflow from a password reset email.
$key = $request->getStr('key');
$password_type = PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE;
$token = null;
if ($key) {
$token = id(new PhabricatorAuthTemporaryTokenQuery())
->setViewer($user)
->withTokenResources(array($user->getPHID()))
->withTokenTypes(array($password_type))
->withTokenCodes(array(PhabricatorHash::weakDigest($key)))
->withExpired(false)
->executeOne();
}
// NOTE: Users can also change passwords through the separate "set/reset"
// interface which is reached by logging in with a one-time token after
// registration or password reset. If this flow changes, that flow may
// also need to change.
$e_old = true;
$e_new = true;
@ -59,12 +46,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
$errors = array();
if ($request->isFormPost()) {
if (!$token) {
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
if (!$user->comparePassword($envelope)) {
$errors[] = pht('The old password you entered is incorrect.');
$e_old = pht('Invalid');
}
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
if (!$user->comparePassword($envelope)) {
$errors[] = pht('The old password you entered is incorrect.');
$e_old = pht('Invalid');
}
$pass = $request->getStr('new_pw');
@ -98,16 +83,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
unset($unguarded);
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 = '/';
} else {
$next = $this->getPanelURI('?saved=true');
}
$next = $this->getPanelURI('?saved=true');
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
$user,
@ -125,19 +101,15 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
$can_upgrade = false;
// Only show this stuff if we aren't on the reset workflow. We can
// do resets regardless of the old hasher's availability.
if (!$token) {
$errors[] = pht(
'Your password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used or updated.');
$errors[] = pht(
'To set a new password, request a password reset link from the '.
'login screen and then follow the instructions.');
}
$errors[] = pht(
'Your password is currently hashed using an algorithm which is '.
'no longer available on this install.');
$errors[] = pht(
'Because the algorithm implementation is missing, your password '.
'can not be used or updated.');
$errors[] = pht(
'To set a new password, request a password reset link from the '.
'login screen and then follow the instructions.');
}
if ($can_upgrade) {
@ -153,20 +125,13 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
$len_caption = pht('Minimum password length: %d characters.', $min_len);
}
$form = new AphrontFormView();
$form
->setUser($user)
->addHiddenInput('key', $key);
if (!$token) {
$form->appendChild(
$form = id(new AphrontFormView())
->setViewer($user)
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('Old Password'))
->setError($e_old)
->setName('old_pw'));
}
$form
->setName('old_pw'))
->appendChild(
id(new AphrontFormPasswordControl())
->setDisableAutocomplete(true)