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 @@ -2112,6 +2112,7 @@ '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 @@ 'PhabricatorAuthSessionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthSessionInfo' => 'Phobject', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', 'PhabricatorAuthTOTPKeyTemporaryTokenType' => 'PhabricatorAuthTemporaryTokenType', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -84,6 +84,7 @@ => 'PhabricatorAuthSSHKeyDeactivateController', 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), + 'password/' => 'PhabricatorAuthSetPasswordController', ), '/oauth/(?P\w+)/login/' diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -139,8 +139,7 @@ ->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( diff --git a/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php new file mode 100644 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSetPasswordController.php @@ -0,0 +1,155 @@ +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); + } +} 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 @@ -262,6 +262,10 @@ PhabricatorPeopleUserPHIDType::TYPECONST); } + public function hasPassword() { + return (bool)strlen($this->passwordHash); + } + public function setPassword(PhutilOpaqueEnvelope $envelope) { if (!$this->getPHID()) { throw new Exception( diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -35,23 +35,10 @@ $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 @@ $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 @@ 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 @@ } 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 @@ $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)