Skip to content
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

feat(#9547): add password change feature on first time login #9581

Open
wants to merge 62 commits into
base: master
Choose a base branch
from

Conversation

Benmuiruri
Copy link
Contributor

@Benmuiruri Benmuiruri commented Oct 24, 2024

Description

Password reset for offline user
Screen.Recording.2024-11-18.at.10.45.46.mov

Closes #9547

Code review checklist

  • Readable: Concise, well named, follows the style guide, documented if necessary.
  • Documented: Configuration and user documentation on cht-docs
  • Tested: Unit and/or e2e where appropriate
  • Internationalised: All user facing text
  • Backwards compatible: Works with existing data and configuration or includes a migration. Any breaking changes documented in the release notes.

Compose URLs

If Build CI hasn't passed, these may 404:

License

The software is provided under AGPL-3.0. Contributions to this project are accepted under the same license.

@Benmuiruri
Copy link
Contributor Author

Hi @garethbowen I worked on the first part to understand the API side routing and display the password page.
Now I'm updating the form to meet the Figma design and checking out the update password logic to use it in the password change form. As I do that, I'd like your input of my approach / understanding of this feature.

  • The approach I would like to take for this is to edit the add user form in the admin to add a checkbox option, which is unchecked by default (hence false / null). When checked it sets require_password_change to true in the user object.
  • In the login controller I have password_change_required = true;.
  • The login controller will check the permission (does the user's role have the permission can_change_password_first_login && is require_password_change = true for the user && is password_change_required = true; ) - If all those are true, then it is the user's first time login and require password change.
  • Once user changes password the password update function will be in the login controller so that it can update password_change_required = false; and possibly update user settings to update require_password_change flag to false That way subsequent logins will not require password change.

My Thoughts

  • I believe this would work with CHT User Management tool to add the checkbox option when creating single or multiple users.
  • I believe this would work if a user loses their phone because the admin can select the require_password_change checkbox in the admin.
  • I believe this would not be a breaking change for existing users because require_password_change would be null for them.
  • When making it mandatory for projects in future, we would make the require_password_change field to be mandatory true.
  • A forgotten password feature would also reuse the same require_password_change in the user object.

@garethbowen
Copy link
Member

The approach I would like to take for this is to edit the add user form in the admin to add a checkbox option, which is unchecked by default (hence false / null). When checked it sets require_password_change to true in the user object.

Unfortunately this does not solve the requirement. The requirement is that every time a password is set by anyone other than the user themselves (ie: admin), the user is required to set a new password on login. This should be enforced in the API somewhere to ensure this happens regardless of whether the password is set via the admin app, the user management app, or any other method. Therefore there must be no checkbox in the admin app to turn this off. You'll have to check how the user management app is written to make sure it applies there as well.

Because this may be disruptive we will include a feature flag to turn it off, just like we do for the new UX features like the action bar -> floating action button. In some time if/when this has been proven to work we will remove the feature flag and make this the only option for all user password setting.

All of this must default to true, otherwise we cannot claim the CHT is secure by default.

I believe this would work with CHT User Management tool to add the checkbox option when creating single or multiple users.

It's also possible to set up the user with scripts and so on, so I prefer the API approach so we can catch all cases where the password is set.

I believe this would work if a user loses their phone because the admin can select the require_password_change checkbox in the admin.

If the phone is lost then the administrator MUST change the password, not set the checkbox. You have to change the password in order to invalidate the session so anyone finding the phone is kicked out of the app. If they're changing the password then the process above (mandatory password reset) will mean you don't need the checkbox at all.

I believe this would not be a breaking change for existing users because require_password_change would be null for them.

Thanks for thinking about this! That's an absolute requirement.

@Benmuiruri Benmuiruri marked this pull request as ready for review October 31, 2024 18:17
@Benmuiruri
Copy link
Contributor Author

Hi @latin-panda this is ready for an early review even though it is not 100% done. A couple of things:

  • I tried a couple of things to display the param for the translation, but I could not get the minimum to display, any idea how to pass it from the js file to the html file ?
  • I split the translation for login and password reset to api/src/public/login/auth-utils.js to avoid duplication. Do I change the eslint to allow compile app to run without the Parsing error: 'import' and 'export' may appear only with 'sourceType: module' error ?
  • I'll work on the styling of the password reset validation user feedback to match Figma on Monday.

With those three points, I think the solution design is ready for an early review.

Copy link
Contributor

@latin-panda latin-panda left a comment

Choose a reason for hiding this comment

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

Nice job Ben! 🤩

Please test that password values don't appear in logs for weird error cases

api/resources/translations/messages-es.properties Outdated Show resolved Hide resolved
api/resources/translations/messages-en.properties Outdated Show resolved Hide resolved
api/src/auth.js Outdated Show resolved Hide resolved
api/src/controllers/login.js Outdated Show resolved Hide resolved
api/resources/translations/messages-en.properties Outdated Show resolved Hide resolved
api/src/public/login/auth-utils.js Show resolved Hide resolved
api/src/public/login/auth-utils.js Outdated Show resolved Hide resolved
api/src/public/login/auth-utils.js Outdated Show resolved Hide resolved
api/src/public/login/password-reset.js Outdated Show resolved Hide resolved
api/src/public/login/password-reset.js Outdated Show resolved Hide resolved
@Benmuiruri
Copy link
Contributor Author

Hi @garethbowen Thanks for your review.

  • I refactored the auth-utils.js file to drop the use of modules
  • I changed the updatePassword in the login controller to safe guard against timeouts over a network
  • I added the currentPassword field in the password reset form
  • I added a check for password reset in the authorization middleware
This video shows the progress so far
Screen.Recording.2024-11-18.at.10.45.46.mov

I'm currently updating failing e2e tests (almost done). As I continues, could I get your review of my implementation of your requested changes. Thanks

Copy link
Member

@garethbowen garethbowen left a comment

Choose a reason for hiding this comment

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

Great progress! I've left a few smallish things to work on.

It's now a fairly big change - once you make these changes and fix the tests I'll come back to it with fresh eyes and give it a thorough look.

api/src/auth.js Outdated Show resolved Hide resolved
api/src/auth.js Outdated Show resolved Hide resolved
api/tests/mocha/middleware/authorization.spec.js Outdated Show resolved Hide resolved
@Benmuiruri
Copy link
Contributor Author

Hey @jkuester 👋

With @garethbowen out of office this week, I was wondering if you could review this PR, we work together to get it merged.

I addressed the feedback, but I'd appreciate a fresh pair of eyes given it was my first time in the API side of things.

The PR description has a video of the state of the feature. Happy to work with you on this.

@jkuester
Copy link
Contributor

jkuester commented Dec 2, 2024

👍 Sounds good @Benmuiruri. I can have a look at things and let you know if it would be helpful to sync on the details.

Unfortunately, I am pretty stacked up this week with PRs. I should be able to get to this by EOW, but if you need it sooner, feel free to replace me with a different reviewer.

@jkuester jkuester self-requested a review December 5, 2024 22:08
@jkuester
Copy link
Contributor

jkuester commented Dec 5, 2024

FYI, got dug in on this and started reviewing it today, but was not able to make it all the way through. I will be OOO on Fri/Mon, but will pick this back up next Tues. 👍

@Benmuiruri
Copy link
Contributor Author

Thanks for the heads up @jkuester

Copy link
Contributor

@jkuester jkuester left a comment

Choose a reason for hiding this comment

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

Alright, going to go ahead and post what I have reviewed so far. You have done a TON of great work here! (Hard to believe how much code it took to implement this feature... 😅 )

I have tried to focus just on important issues/questions and/or easy code suggestions. But, knowing the time constraints we have here, I will definitely accept "This works fine as it is" as a response to any of my refactoring comments!

shared-libs/user-management/src/index.js Show resolved Hide resolved
shared-libs/user-management/src/users.js Show resolved Hide resolved
shared-libs/user-management/src/users.js Show resolved Hide resolved
shared-libs/user-management/src/users.js Show resolved Hide resolved
}

const userDoc = await db.users.get(`org.couchdb.user:${username}`);
await updatePassword(userDoc, password);
Copy link
Contributor

Choose a reason for hiding this comment

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

question: Actually, wait, is there a reason we cannot use the users.updateUser functionality here instead of manually getting/putting the _user doc? Not sure what the 401 retry logic is about in this updatePassword function, though. If we need that, then I guess we cannot use users.updateUser...

Copy link
Contributor

Choose a reason for hiding this comment

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

The longer I look at this, the more confused I am by that retry logic. Why would we get a 401 on the put, but not here on the get? I am probably missing something important here....

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's because of a race condition, when testing it out, in some instances requesting session immediately after changing a password was giving back a 401. Hence I safe guarded it with the retry to avoid that.

Copy link
Contributor

Choose a reason for hiding this comment

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

So, yeah, the createSessionRetry logic makes sense to me (and I guess already existed for token login). But here is where we are actually changing the password. Seems like we should not be getting a 401 from the db.users.put(updatedUser); call. If I understand things correctly, the user session that is actually used to make that update would be the api server's admin user (COUCH_USER) anyway since we have not yet created a session for the current user trying to login....

api/src/controllers/login.js Show resolved Hide resolved
api/tests/mocha/controllers/login.spec.js Show resolved Hide resolved
api/src/controllers/login.js Show resolved Hide resolved
api/src/controllers/login.js Show resolved Hide resolved
Copy link
Contributor

@jkuester jkuester left a comment

Choose a reason for hiding this comment

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

Okay, made it through the rest of the implementation code and unit tests.

For the e2e/integration tests, I don't like how all the random tests that need a non-admin user have to deal with the password change complexity. Ideally we would have dedicated tests for the password reset workflow and then none of the rest of the tests woud have to worry about it.

Now that is, of course, easier said than done. I guess the most naive way to avoid the password reset would be to add the roles to can_skip_password_change in config/default/app_settings.json, but that is not super elegant.

The main idea I have come up with so far involves updaing the createUsers function in utils/index.js to do either of these two things:

  • Manually update the user doc after creating the user to remove the password_change_required field. This is nice and simple. The downside is that it is a bit artificial, but I am not sure it rises to the level of affecting the integrety of the tests.
  • The other alternative would be to have the createUsers function write the initial user with with a different password than the one provided in the parameter. Then, after the user has been created the createUsers function could do a login and run the reset-password flow to set the password to the original password value provided for the user. The end result after calling createUsers would be that the user exists and has the specified password and a reset will not be triggered the next time the user logs in.

Ideally the deafult behavior of createUsers would result in the user existing in the db with the provided password and no password reset would happen the next time the user logs in. For the actual password-reset tests, we could either parameterize the createUsers function to be able to flag off that behavior (so the test can do the reset) or we could just not use this function for creating users for those tests...

@@ -40,7 +40,9 @@ module.exports = {
return serverUtils.error('Authentication error', req, res);
}

next();
return auth.checkPasswordChange(req)
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, do we really need to force this check here? If we only set the password_change_required when actually changing the user's password, then the user's session should always be invalidated. This would require them to re-login and then they will hit the password_change_required logic in the login controller. Are there other cases I am missing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason for adding this was to block users who require password change from bypassing the check via basic auth or curl.

  • The first result is with the guard
  • The second result is without it
Screenshot 2024-12-13 at 17 11 20

Copy link
Contributor

Choose a reason for hiding this comment

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

Ahh, I see. 🤔 Honestly, I wonder if we actually care about the basic auth case... 😬 They are giving is a valid password, so it is not like this is a security hole. Maybe it is acceptable to say that the password-reset flow is only triggered by the login-flow (and not when making random requests with basic auth...). IMHO that would be preferable to adding an extra round-trip to _users for each server request....

Going to push this question up to the main issue too!

api/src/auth.js Show resolved Hide resolved
api/src/auth.js Show resolved Hide resolved
api/src/services/cookie.js Show resolved Hide resolved
@@ -99,6 +99,12 @@ describe('bootstrapper', () => {
return promise;
});

global.localStorage = {
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: can we also get a few tests to cover the new logic?

@@ -31,7 +31,8 @@ describe('Acessing the admin app', () => {
const error = '{"code":403,"error":"forbidden","details":"Offline users are not allowed access to this endpoint"}';
await utils.saveDocs([parent]);
await utils.createUsers([offlineUser]);
await loginPage.cookieLogin({ ...offlineUser, createUser: false });

await loginPage.login({ username: offlineUser.username, password: offlineUser.password });
Copy link
Contributor

Choose a reason for hiding this comment

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

question: why did this need to change?

return !user?.password_change_required ||
user?.token_login?.active ||
roles.isDbAdmin(userCtx) ||
await auth.hasAllPermissions(userCtx, 'can_skip_password_change');
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: referencing back to my previous comment about how this feature feels like a bit of a breaking change to have enabled by default, I initially expected this behavior to be something toggled ON by a permission. E.g. the system would trigger the reset password flow only for users with the can_reset_password_assigned_by_admin permission, or something like that...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great observation, I initially thought that as well. Even started working on it from that approach. However, Gareth raised a fair point. Our purpose for this feature is to improve security to safe guard against a password leak.

If we leave it as an opt in feature, because people are generally "lazy" when it comes to security steps, a project will not turn it on. Therefore, in the event of a password leak, the feature won't really help them.

But having it as an opt out then that way the system is designed to safe guard, but you can opt out knowingly at your own risk.

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.

Change password on first login
6 participants