-
-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Added verify password to users router and tests. #4747
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
Added verify password to users router and tests. #4747
Conversation
Codecov Report
@@ Coverage Diff @@
## master #4747 +/- ##
==========================================
- Coverage 92.74% 92.73% -0.01%
==========================================
Files 119 119
Lines 8722 8732 +10
==========================================
+ Hits 8089 8098 +9
- Misses 633 634 +1
Continue to review full report at Codecov.
|
… function where authData null keys condition wasn't necessary.
Hi there! I have updated the code to pass the checks this time around. I understand it might be a heavy request to add an additional API endpoint to parse-server like this, so if there are hesitations, I'd love to know about them! I am also willing to follow through by adding this to the Parse JS SDK so we can get a Parse.User.verifyPassword function, as well as adding it to the appropriate documentation like the REST API docs and javascript docs. Let me know if there's anything else I can do to support or if I can explain with any more reasoning. Thanks so much! |
That sounds like a great feature! Is there any particular reason we have both GET and POST handling for this feature? Because nothing gets created server side, I believe the right method would be just GET. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnnydimas The Tests look good, if everything is fine, I'd like to have the code shared between login and verifyPassword, rather than duplicated, as I will ease maintenance, and Login is just validating pass + creating a session :)
Can you update the PR and we'll be able to make a release :)
src/Routers/UsersRouter.js
Outdated
@@ -267,6 +267,77 @@ export class UsersRouter extends ClassesRouter { | |||
}); | |||
} | |||
|
|||
handleVerifyPassword(req) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@johnnydimas this whole section could be factored differently, as this is shared 100% with the login code right?
We could probably have a function somewhere that handles password validation, in both login / verifyPassword and only create the session on login? What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds great to me! Good call on removing the POST handler as well, it's not necessary for this.
I pushed up a fix removing the POST now, but I will update the pull request again with a helper to facilitate both login and verify password.
Thanks for looking it over!
…function to validate the password provided in the request.
I have created a shared helper function to reduce the duplicate code between login and verify password that validates the password, and I kept the create session code in the login function. I noticed there is this bit of code in there that I was wondering if we should remove or keep. I followed the url in the comment and it seems that this issue has since been addressed. Let me know, and I can update the PR. 😄
Thanks! |
src/Routers/UsersRouter.js
Outdated
// password was created before expiry policy was enabled. | ||
// simply update _User object so that it will start enforcing from now | ||
changedAt = new Date(); | ||
req.config.database.update('_User', { username: user.username }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this part feels odd that we keep it in a verification, when the password is actually not changed there. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense! I'll move this snippet back into the handleLogin function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exellent thanks! That,s quite the refactor you're on, I want to get it right before we merge ;)
src/Routers/UsersRouter.js
Outdated
}); | ||
} | ||
|
||
handleLogIn(req) { | ||
try { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a big fan of wrapping in try's why do we need those? the top level promise should never throw but return a failed promise. Can we change that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can update this to remove the try / catch's :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nits otherwise, I like this level of code sharing!
src/Routers/UsersRouter.js
Outdated
return { response: user }; | ||
}); | ||
} catch (error) { | ||
throw error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if we never to anything with the errors, please remove all try wrappers ;)
…ise returns the error. Moved login specific functions to login handler.
I removed all the try / catch's and I moved the login specific code into that function. Let me know if you see any other issues with the PR. Thanks! :) |
Hey @flovilmart ! Just curious if there was anything else you'd like to see done here, happy to make any more edits. No rush and thanks! :) |
Hi @johnnydimas there seems to be a conflict now, can you address it? I'll do a final review today. |
Hey! I'm working on the merge now and adding a test for the account lockout in verify password. |
Awesome thanks! |
…spec for account lockout in verify password.
… into verify_password # Conflicts: # .github/ISSUE_TEMPLATE/Bug_report.md
I sorted out the merge conflict, added the lockout check to the shared helper function, and added a new test for the account lockout feature to verify password. :) I had a weird issue with the .github/ISSUE_TEMPLATE/Bug_report.md file, and had to push it again to the branch for some reason. It looks to be exactly the same though. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, last small change and we'll be good to go. We need the same level of cleanup on both methods. THe only difference is that one is creating the session the other isn't.
src/Routers/UsersRouter.js
Outdated
@@ -162,13 +182,20 @@ export class UsersRouter extends ClassesRouter { | |||
delete user.authData; | |||
} | |||
} | |||
|
|||
// Remove hidden properties. | |||
UsersRouter.removeHiddenProperties(user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be moved to the _handleValidatePassword
method as also shared with the other call.
@@ -162,13 +182,20 @@ export class UsersRouter extends ClassesRouter { | |||
delete user.authData; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all this block shoudl be put in the extracted method as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the issue here was if I move these two blocks to the _authenticateUserFromRequest function:
// Sometimes the authData still has null on that keys
// https://github.com/parse-community/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
// Remove hidden properties.
UsersRouter.removeHiddenProperties(user);
Three of PasswordPolicy tests for expiration will fail. It seems the order matters, here. The way I can see to solve it would be if I move this block into the _authenticateUserFromRequest as well so that the order is maintained, and so that this occurs before the authData check and removing hidden properties:
// handle password expiry policy
if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) {
let changedAt = user._password_changed_at;
if (!changedAt) {
// password was created before expiry policy was enabled.
// simply update _User object so that it will start enforcing from now
changedAt = new Date();
req.config.database.update('_User', { username: user.username },
{ _password_changed_at: Parse._encode(changedAt) });
} else {
// check whether the password has expired
if (changedAt.__type == 'Date') {
changedAt = new Date(changedAt.iso);
}
// Calculate the expiry time.
const expiresAt = new Date(changedAt.getTime() + 86400000 * req.config.passwordPolicy.maxPasswordAge);
if (expiresAt < new Date()) // fail of current time is past password expiry time
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.');
}
}
Is it ok to have this entire chunk of code moved into _authenticateUserFromRequest so it is like so?
// handle password expiry policy
if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) {
let changedAt = user._password_changed_at;
if (!changedAt) {
// password was created before expiry policy was enabled.
// simply update _User object so that it will start enforcing from now
changedAt = new Date();
req.config.database.update('_User', { username: user.username },
{ _password_changed_at: Parse._encode(changedAt) });
} else {
// check whether the password has expired
if (changedAt.__type == 'Date') {
changedAt = new Date(changedAt.iso);
}
// Calculate the expiry time.
const expiresAt = new Date(changedAt.getTime() + 86400000 * req.config.passwordPolicy.maxPasswordAge);
if (expiresAt < new Date()) // fail of current time is past password expiry time
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.');
}
}
// Sometimes the authData still has null on that keys
// https://github.com/parse-community/parse-server/issues/935
if (user.authData) {
Object.keys(user.authData).forEach((provider) => {
if (user.authData[provider] === null) {
delete user.authData[provider];
}
});
if (Object.keys(user.authData).length == 0) {
delete user.authData;
}
}
// Remove hidden properties.
UsersRouter.removeHiddenProperties(user);
delete user.password;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll have a look because. Seems that we need to keep is split and clean.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Had you wanted me to push with the changes I did as detailed above so you could look into it? Thanks!
src/Routers/UsersRouter.js
Outdated
.then((user) => { | ||
|
||
// Remove hidden properties. | ||
UsersRouter.removeHiddenProperties(user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove as it will be put above.
src/Routers/UsersRouter.js
Outdated
* @returns {Object} User object | ||
* @private | ||
*/ | ||
_handleValidatePassword(req) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should rename to _authenticateUserFromRequest(req)
what do you think?
THie method retuns the user, stripped of the private / hidden properties etc...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem, I can rename it.
…o the login function. The password expiration check in the login function is dependent on some hidden properties, otherwise three password policy tests fail.
Hey @flovilmart ! I took another crack at this one. I wasn't able to share the Thanks! |
@johnnydimas this looks great! It seems that all the tests are still passing and the additional tests look great. let's merge that, I don't believe we can hit regressions ! 👏 Thanks for keeping up! |
* Added verify password to users router and tests. * Added more tests to support more coverage. * Added additional tests to spec. Removed condition from verifyPassword function where authData null keys condition wasn't necessary. * Removed POST handling from verifyPassword. * Refactored handleLogin and handleVerifyPassword to use shared helper function to validate the password provided in the request. * Refactored verifyPassword and login to not use try/catch. Parent promise returns the error. Moved login specific functions to login handler. * Added account lockout policy to verify password function. Added test spec for account lockout in verify password. * no message * Merged new changes from master. Made changes as requested from comments. * We cannot remove hidden properties from the helper before returning to the login function. The password expiration check in the login function is dependent on some hidden properties, otherwise three password policy tests fail.
* Added verify password to users router and tests. * Added more tests to support more coverage. * Added additional tests to spec. Removed condition from verifyPassword function where authData null keys condition wasn't necessary. * Removed POST handling from verifyPassword. * Refactored handleLogin and handleVerifyPassword to use shared helper function to validate the password provided in the request. * Refactored verifyPassword and login to not use try/catch. Parent promise returns the error. Moved login specific functions to login handler. * Added account lockout policy to verify password function. Added test spec for account lockout in verify password. * no message * Merged new changes from master. Made changes as requested from comments. * We cannot remove hidden properties from the helper before returning to the login function. The password expiration check in the login function is dependent on some hidden properties, otherwise three password policy tests fail.
* Added verify password to users router and tests. * Added more tests to support more coverage. * Added additional tests to spec. Removed condition from verifyPassword function where authData null keys condition wasn't necessary. * Removed POST handling from verifyPassword. * Refactored handleLogin and handleVerifyPassword to use shared helper function to validate the password provided in the request. * Refactored verifyPassword and login to not use try/catch. Parent promise returns the error. Moved login specific functions to login handler. * Added account lockout policy to verify password function. Added test spec for account lockout in verify password. * no message * Merged new changes from master. Made changes as requested from comments. * We cannot remove hidden properties from the helper before returning to the login function. The password expiration check in the login function is dependent on some hidden properties, otherwise three password policy tests fail.
We needed a way in our app to verify that a user knows their current password to proceed past some functions for additional security. One example could be that we would like a way for a user to update their password from within the app while logged in, but by first submitting their 'old password'. We tried to achieve this by logging the user in server-side, but then their current session tokens would become invalid and log them out of the app, and it did not really make sense to have to send a new session token back again.
There are also some other instances where this could come in handy, where logging a user in again or using the password reset email system isn't so optimal.
In short I added a route for verifying a user password by comparing the sent in password string against the hash that is saved in the database. If the password isn't a match, then appropriate errors are returned that are the same for login errors.
If the comparison is successful, the user object (without a session token) is returned.
Couple other things to note:
If account lockout is configured in the parse-server, this function will also makes use of the account lockout policy if the wrong password is sent in multiple times.
This function does not modify session tokens.
A test spec for this function was also added.
Thanks!