mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-30 02:32:42 +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:
parent
66b74073be
commit
ad4db9b2f3
6 changed files with 185 additions and 59 deletions
|
@ -2112,6 +2112,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
|
'PhabricatorAuthSessionGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthSessionGarbageCollector.php',
|
||||||
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
|
'PhabricatorAuthSessionInfo' => 'applications/auth/data/PhabricatorAuthSessionInfo.php',
|
||||||
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
|
'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php',
|
||||||
|
'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php',
|
||||||
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
|
'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php',
|
||||||
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
|
'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php',
|
||||||
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
|
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'applications/auth/factor/PhabricatorAuthTOTPKeyTemporaryTokenType.php',
|
||||||
|
@ -7377,6 +7378,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
|
'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector',
|
||||||
'PhabricatorAuthSessionInfo' => 'Phobject',
|
'PhabricatorAuthSessionInfo' => 'Phobject',
|
||||||
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||||
|
'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
|
'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck',
|
||||||
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
|
'PhabricatorAuthStartController' => 'PhabricatorAuthController',
|
||||||
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
|
'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType',
|
||||||
|
|
|
@ -84,6 +84,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication {
|
||||||
=> 'PhabricatorAuthSSHKeyDeactivateController',
|
=> 'PhabricatorAuthSSHKeyDeactivateController',
|
||||||
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
|
'view/(?P<id>\d+)/' => 'PhabricatorAuthSSHKeyViewController',
|
||||||
),
|
),
|
||||||
|
'password/' => 'PhabricatorAuthSetPasswordController',
|
||||||
),
|
),
|
||||||
|
|
||||||
'/oauth/(?P<provider>\w+)/login/'
|
'/oauth/(?P<provider>\w+)/login/'
|
||||||
|
|
|
@ -139,8 +139,7 @@ final class PhabricatorAuthOneTimeLoginController
|
||||||
->save();
|
->save();
|
||||||
unset($unguarded);
|
unset($unguarded);
|
||||||
|
|
||||||
$username = $target_user->getUsername();
|
$panel_uri = '/auth/password/';
|
||||||
$panel_uri = "/settings/user/{$username}/page/password/";
|
|
||||||
|
|
||||||
$next = (string)id(new PhutilURI($panel_uri))
|
$next = (string)id(new PhutilURI($panel_uri))
|
||||||
->setQueryParams(
|
->setQueryParams(
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -262,6 +262,10 @@ final class PhabricatorUser
|
||||||
PhabricatorPeopleUserPHIDType::TYPECONST);
|
PhabricatorPeopleUserPHIDType::TYPECONST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasPassword() {
|
||||||
|
return (bool)strlen($this->passwordHash);
|
||||||
|
}
|
||||||
|
|
||||||
public function setPassword(PhutilOpaqueEnvelope $envelope) {
|
public function setPassword(PhutilOpaqueEnvelope $envelope) {
|
||||||
if (!$this->getPHID()) {
|
if (!$this->getPHID()) {
|
||||||
throw new Exception(
|
throw new Exception(
|
||||||
|
|
|
@ -35,23 +35,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
|
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
|
||||||
$min_len = (int)$min_len;
|
$min_len = (int)$min_len;
|
||||||
|
|
||||||
// NOTE: To change your password, you need to prove you own the account,
|
// NOTE: Users can also change passwords through the separate "set/reset"
|
||||||
// either by providing the old password or by carrying a token to
|
// interface which is reached by logging in with a one-time token after
|
||||||
// the workflow from a password reset email.
|
// registration or password reset. If this flow changes, that flow may
|
||||||
|
// also need to change.
|
||||||
$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();
|
|
||||||
}
|
|
||||||
|
|
||||||
$e_old = true;
|
$e_old = true;
|
||||||
$e_new = true;
|
$e_new = true;
|
||||||
|
@ -59,12 +46,10 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
|
|
||||||
$errors = array();
|
$errors = array();
|
||||||
if ($request->isFormPost()) {
|
if ($request->isFormPost()) {
|
||||||
if (!$token) {
|
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
|
||||||
$envelope = new PhutilOpaqueEnvelope($request->getStr('old_pw'));
|
if (!$user->comparePassword($envelope)) {
|
||||||
if (!$user->comparePassword($envelope)) {
|
$errors[] = pht('The old password you entered is incorrect.');
|
||||||
$errors[] = pht('The old password you entered is incorrect.');
|
$e_old = pht('Invalid');
|
||||||
$e_old = pht('Invalid');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$pass = $request->getStr('new_pw');
|
$pass = $request->getStr('new_pw');
|
||||||
|
@ -98,16 +83,7 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
|
|
||||||
unset($unguarded);
|
unset($unguarded);
|
||||||
|
|
||||||
if ($token) {
|
$next = $this->getPanelURI('?saved=true');
|
||||||
// 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');
|
|
||||||
}
|
|
||||||
|
|
||||||
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
id(new PhabricatorAuthSessionEngine())->terminateLoginSessions(
|
||||||
$user,
|
$user,
|
||||||
|
@ -125,19 +101,15 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
|
} catch (PhabricatorPasswordHasherUnavailableException $ex) {
|
||||||
$can_upgrade = false;
|
$can_upgrade = false;
|
||||||
|
|
||||||
// Only show this stuff if we aren't on the reset workflow. We can
|
$errors[] = pht(
|
||||||
// do resets regardless of the old hasher's availability.
|
'Your password is currently hashed using an algorithm which is '.
|
||||||
if (!$token) {
|
'no longer available on this install.');
|
||||||
$errors[] = pht(
|
$errors[] = pht(
|
||||||
'Your password is currently hashed using an algorithm which is '.
|
'Because the algorithm implementation is missing, your password '.
|
||||||
'no longer available on this install.');
|
'can not be used or updated.');
|
||||||
$errors[] = pht(
|
$errors[] = pht(
|
||||||
'Because the algorithm implementation is missing, your password '.
|
'To set a new password, request a password reset link from the '.
|
||||||
'can not be used or updated.');
|
'login screen and then follow the instructions.');
|
||||||
$errors[] = pht(
|
|
||||||
'To set a new password, request a password reset link from the '.
|
|
||||||
'login screen and then follow the instructions.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($can_upgrade) {
|
if ($can_upgrade) {
|
||||||
|
@ -153,20 +125,13 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel {
|
||||||
$len_caption = pht('Minimum password length: %d characters.', $min_len);
|
$len_caption = pht('Minimum password length: %d characters.', $min_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
$form = new AphrontFormView();
|
$form = id(new AphrontFormView())
|
||||||
$form
|
->setViewer($user)
|
||||||
->setUser($user)
|
->appendChild(
|
||||||
->addHiddenInput('key', $key);
|
|
||||||
|
|
||||||
if (!$token) {
|
|
||||||
$form->appendChild(
|
|
||||||
id(new AphrontFormPasswordControl())
|
id(new AphrontFormPasswordControl())
|
||||||
->setLabel(pht('Old Password'))
|
->setLabel(pht('Old Password'))
|
||||||
->setError($e_old)
|
->setError($e_old)
|
||||||
->setName('old_pw'));
|
->setName('old_pw'))
|
||||||
}
|
|
||||||
|
|
||||||
$form
|
|
||||||
->appendChild(
|
->appendChild(
|
||||||
id(new AphrontFormPasswordControl())
|
id(new AphrontFormPasswordControl())
|
||||||
->setDisableAutocomplete(true)
|
->setDisableAutocomplete(true)
|
||||||
|
|
Loading…
Reference in a new issue