Skip to content

fix: improve password validation flow #15132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Nov 5, 2024
Merged

fix: improve password validation flow #15132

merged 21 commits into from
Nov 5, 2024

Conversation

defelmnq
Copy link
Contributor

@defelmnq defelmnq commented Oct 18, 2024

Refers to #14984

Currently, password validation is done backend side and is not explicit enough so it can be painful to create first users.
We'd like to make this validation easier - but also duplicate it frontend side to make it smoother.

Flows involved :

  • First user set password
  • New user set password
  • Change password

@defelmnq defelmnq self-assigned this Oct 18, 2024
Copy link

github-actions bot commented Oct 18, 2024

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@defelmnq defelmnq force-pushed the fix-password-validation branch from 07708bb to b93dc6b Compare October 18, 2024 02:26
@defelmnq defelmnq changed the title fix(pages): improve password validation flow on first user setup fix: improve password validation flow on first user setup Oct 18, 2024
@mtojek mtojek requested a review from johnstcn October 18, 2024 07:50
Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in two minds about this change.

On the one hand, I agree that the entropy-based approach is obtuse and annoying, and that it is in some way the user's responsibility to have a minimum password security value. Also, if security is your top priority, password authentication is maybe not the best approach.

On the other hand, allowing folks to just use password or abc123 feels wrong to me, and this seems like the kind of thing a security audit would find deeply wanting.

I considered the below rules:

  1. Mandate use of at least one character from each of the following classes: [a-z] [A-Z] [0-9]
  2. Mandate minimum length 9

However this would immediately discount passwords consisting entirely of non-English υηι¢σ∂є ¢нαяα¢тєяѕ into account.

Here's an idea: given that the entropy calculation check is relatively cheap, would it be a terrible idea to expose an endpoint to validate this? The frontend could then consume this and show the entropy calculation transparently.

EDIT: alternative proposition: adjust the rule-based approach to

  1. At least one non-alphanumeric non-whitespace character (e.g. [^a-zA-Z0-9\s]+)
  2. Minimum length of 9

@BrunoQuaresma
Copy link
Collaborator

I think the way you did it is fine, but I’d suggest trying to use Yup features to simplify it since we’re already using it. Reposting from Slack:

Ok, cool. The good thing is, you can already do something like that using Yup, the validation library we use. You can check out an example on this StackOverflow question. You can probably place it in the formUtils.ts file.

Sorry for the late message 🙏

@defelmnq
Copy link
Contributor Author

I think the way you did it is fine, but I’d suggest trying to use Yup features to simplify it since we’re already using it. Reposting from Slack:

Ok, cool. The good thing is, you can already do something like that using Yup, the validation library we use. You can check out an example on this StackOverflow question. You can probably place it in the formUtils.ts file.

Sorry for the late message 🙏

Hey @BrunoQuaresma thanks for the review and validation on this piece of code. A bit more context can be found here but globally the idea is to keep frontend validation as small as possible - (probably just for the obvious things) and then have a debounced backend one for the more detailed validation - so we ensure consistent validation everywhere.

I'll put the same kind of changes in all other password fields - so the PR will be fully reviewable.

err := passwordvalidator.Validate(password, 52)
if err != nil {
return err
if len(password) < 6 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now this logic stays pretty simple - here's the place where we'll be able to apply all the validation logic we want

Copy link
Member

@johnstcn johnstcn Oct 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I'm concerned here, we are squishing together three distinct separate changes:

  1. Expose a password complexity validation endpoint on the backend
  2. Modify the password validation logic on the FE to query the BE
  3. Modify the password validation logic on the BE

Of these three options, 1) and 2) are relatively innocuous. 3) is the one I'm mainly concerned about.

I propose keeping the existing password validation logic (via entropy) in this PR and opening a separate PR to modify the password validation logic if required.

My proposal for a roughly equivalent non-entropy-based password validation logic is:

  1. At least one non-alphanumeric non-whitespace character ([^a-zA-Z0-9\s])
  2. Minimum length of 9 runes

EDIT: we should however be extremely careful if we change the validation, as users could have existing passwords that are valid per entropy but invalid per the above rule.

@defelmnq defelmnq force-pushed the fix-password-validation branch from 43124c0 to 207d5cc Compare October 23, 2024 15:36
@defelmnq defelmnq force-pushed the fix-password-validation branch from 207d5cc to 388a58b Compare October 23, 2024 15:42
@defelmnq defelmnq changed the title fix: improve password validation flow on first user setup fix: improve password validation flow Oct 23, 2024
@defelmnq defelmnq requested a review from johnstcn October 23, 2024 18:23
@defelmnq defelmnq marked this pull request as ready for review October 23, 2024 18:24
@defelmnq
Copy link
Contributor Author

PR is for me ready for a first review - both frontend and backend side.

We'd just need to :

  • Define what we define as a strong enough password security wise cc @johnstcn (?) and change the rules based on that.
  • Potentially improve the wording frontend side.

@defelmnq
Copy link
Contributor Author

defelmnq commented Oct 24, 2024

Based on conversation @johnstcn - I changed a bit the PR.

We pushed back the validation of the password as a prerequisite. If we then want it to just be a hint, it will be done in another PR.

Also we continue to use entropy-based validation as before.

Edit : @BrunoQuaresma I am not sure yet how validation should be done for storybook ?

@BrunoQuaresma
Copy link
Collaborator

I am not sure yet how validation should be done for storybook ?

Here are the steps that I would take:

  • Create an "invalid password" story for each component that uses it
  • Add an interaction test
  • Mock the API call to return the API error
  • Expect to see if the message is correctly displayed

You can see an example of a "test like this"here. Please, let me know if you this is helpful enough or if you have any additional questions 🙏 .

@defelmnq
Copy link
Contributor Author

Just sharing some screenshot of what it currently looks like for visibility.

Screenshot 2024-10-28 at 16 55 13
Screenshot 2024-10-28 at 16 54 30
Screenshot 2024-10-28 at 16 55 04

@defelmnq defelmnq requested a review from johnstcn October 28, 2024 17:44
@defelmnq defelmnq force-pushed the fix-password-validation branch from 277cb57 to f33eac2 Compare October 28, 2024 17:45
@defelmnq
Copy link
Contributor Author

@johnstcn @BrunoQuaresma

  • I added some frontend tests, not sure how to do for all of them but tried to add some.
  • Overall, I also added the details logic described by Cian - I've not added any logic to hide the user's password yet, because the library itself describe the error as being safe to display for user, but I can add it if you think it's better.

Copy link
Member

@johnstcn johnstcn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backed changes look good to me! @BrunoQuaresma could you take a look at the FE changes?

Comment on lines -79 to -96
test("update password with invalid password", async () => {
jest.spyOn(API, "updateUserPassword").mockRejectedValueOnce(
mockApiError({
message: "Invalid password.",
validations: [{ detail: "Invalid password.", field: "password" }],
}),
);

const { user } = await renderPage();
fillAndSubmitSecurityForm();

const errorMessage = await screen.findAllByText("Invalid password.");
expect(errorMessage).toBeDefined();
expect(errorMessage).toHaveLength(2);
expect(API.updateUserPassword).toBeCalledTimes(1);
expect(API.updateUserPassword).toBeCalledWith(user.id, newSecurityFormValues);
});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we instead modify this test to spy on the validate user password endpoint.

@@ -1322,6 +1322,15 @@ class ApiMethods {
await this.axios.put(`/api/v2/users/${userId}/password`, updatePassword);
};

validateUserPassword = async (
password: string,
Copy link
Member

@Kira-Pilot Kira-Pilot Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be using the ValidateUserPasswordRequest interface here?

// typesGenerated.ts

// From codersdk/users.go
export interface ValidateUserPasswordRequest {
	readonly password: string;
}

@@ -63,6 +63,8 @@ export const authMethodLanguage = {
export interface CreateUserFormProps {
onSubmit: (user: TypesGen.CreateUserRequestWithOrgs) => void;
onCancel: () => void;
onPasswordChange: (password: string) => void;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can also use ValidateUserPasswordRequest here

@@ -9,6 +9,7 @@ import type {
UpdateUserProfileRequest,
User,
UsersRequest,
ValidateUserPasswordRequest,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are we using this?

@@ -104,6 +114,10 @@ export const CreateUserForm: FC<
error,
);

useEffect(() => {
onPasswordChange?.(form.values.password);
}, [form.values.password, onPasswordChange]); // Run effect when password changes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to avoid useEffect as much as possible - it causes unexpected rerenders, which, decoupled with your debouncing function, are going to be difficult to troubleshoot. Instead, I suggest you add an onChange attribute to the TextField component used for the password input. Something like:

onChange={(e) => {
    form.handleChange(e);
    handlePasswordChange(e.target.value);
}}

If you search in the file you can see us using this pattern for other inputs.

@@ -17,6 +18,25 @@ export const CreateUserPage: FC = () => {
const queryClient = useQueryClient();
const createUserMutation = useMutation(createUser(queryClient));
const authMethodsQuery = useQuery(authMethods());
const validatePasswordMutation = useMutation(validatePassword());

const [passwordValidator, setPasswordValidator] = useState({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason all this logic must live in the parent component instead of directly inside CreateUserForm? I could be missing something and @BrunoQuaresma might be able to weigh in.

But if this logic can live in the form component itself, I think it should, for simplicity.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scanning down through the PR, I see that we're duplicating this logic across several views. To avoid doing this, I think we should create a hook called useValidatePassword or something similar, and then call that hook across these components.

I suggest pairing with @BrunoQuaresma or @aslilac if you need help here!

Copy link
Member

@Kira-Pilot Kira-Pilot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking a look at this user improvement and nice job committing some full stack code!

I left some comments on your FE changes. Might be easiest to hop in Discord with one of our UI engineers. LMK if you have any questions!

// @Param request body codersdk.ValidateUserPasswordRequest true "Validate user password request"
// @Success 200 {object} codersdk.ValidateUserPasswordResponse
// @Router /users/validate-password [post]
func (*API) validateUserPassword(rw http.ResponseWriter, r *http.Request) {
Copy link
Member

@aslilac aslilac Nov 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit about referring to all of this stuff as "user password" validation: the password is not yet attached to a user, so it's not a user password, it's just a password, so I'd expect it to be called validatePassword and so on.

@BrunoQuaresma
Copy link
Collaborator

@defelmnq what do you think about the changes? I would also appreciate a QA from you just to ensure I didn't miss any use cases.

Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m self-approving this for now to avoid blocking the PR any longer. If needed, I can take any follow-up actions later on. @defelmnq it is up to you when to merge this PR 🙏

@BrunoQuaresma BrunoQuaresma dismissed Kira-Pilot’s stale review November 5, 2024 12:45

FE requests applied

@defelmnq defelmnq merged commit 4fe2c5f into main Nov 5, 2024
55 of 60 checks passed
@defelmnq defelmnq deleted the fix-password-validation branch November 5, 2024 16:22
@EdwardAngert
Copy link
Contributor

EdwardAngert commented Nov 5, 2024

#15382 (merged to main) fixes the errors in the weekly-docs check that's currently failing

jaaydenh pushed a commit that referenced this pull request Nov 5, 2024
Refers to #14984 

Currently, password validation is done backend side and is not explicit
enough so it can be painful to create first users.
We'd like to make this validation easier - but also duplicate it
frontend side to make it smoother.

Flows involved : 
- First user set password
- New user set password
- Change password

---------

Co-authored-by: BrunoQuaresma <bruno_nonato_quaresma@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants