Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1.2k
Add bcrypt validation#2580
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
Uh oh!
There was an error while loading. Please reload this page.
Add bcrypt validation #2580
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -14,7 +14,7 @@ var utils = require('../../lib/utils'); | ||
| var path = require('path'); | ||
| var SALT_WORK_FACTOR = 10; | ||
| var crypto = require('crypto'); | ||
| var MAX_PASSWORD_LENGTH = 72; | ||
| var bcrypt; | ||
| try { | ||
| // Try the native module first | ||
| @@ -548,7 +548,6 @@ module.exports = function(User) { | ||
| cb = cb || utils.createPromiseCallback(); | ||
| var UserModel = this; | ||
| var ttl = UserModel.settings.resetPasswordTokenTTL || DEFAULT_RESET_PW_TTL; | ||
| options = options || {}; | ||
| if (typeof options.email !== 'string') { | ||
| var err = new Error(g.f('Email is required')); | ||
| @@ -558,6 +557,13 @@ module.exports = function(User) { | ||
| return cb.promise; | ||
| } | ||
| try { | ||
| if (options.password) { | ||
| UserModel.validatePassword(options.password); | ||
| } | ||
| } catch (err) { | ||
| return cb(err); | ||
| } | ||
| UserModel.findOne({ where: { email: options.email }}, function(err, user) { | ||
| if (err) { | ||
| return cb(err); | ||
| @@ -596,14 +602,20 @@ module.exports = function(User) { | ||
| }; | ||
| User.validatePassword = function(plain) { | ||
| if (typeof plain === 'string' && plain) { | ||
| var err; | ||
| if (plain && typeof plain === 'string' && plain.length <= MAX_PASSWORD_LENGTH) { | ||
| return true; | ||
| } | ||
| var err = new Error(g.f('Invalid password: %s', plain)); | ||
| if (plain.length > MAX_PASSWORD_LENGTH) { | ||
| err = new Error (g.f('Password too long: %s', plain)); | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. var err Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually we use the same statusCode for both, we should move it both out. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I should have refactored that ! | ||
| err.code = 'PASSWORD_TOO_LONG'; | ||
| } else { | ||
| err = new Error(g.f('Invalid password: %s', plain)); | ||
| err.code = 'INVALID_PASSWORD'; | ||
| } | ||
| ||
| err.statusCode = 422; | ||
| throw err; | ||
| }; | ||
| /*! | ||
| * Setup an extended user model. | ||
| */ | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,7 +4,7 @@ | ||
| // License text available at https://opensource.org/licenses/MIT | ||
| var SG = require('strong-globalize'); | ||
| SG.SetRootDir(__dirname, {autonomousMsgLoading: 'all'}); | ||
| SG.SetRootDir(__dirname, {autonomousMsgLoading: 'all'}); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this will pass our linter, please check. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah that space was actually added when I ran a linting test. It is still at | ||
| /** | ||
| * loopback ~ public api | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -363,6 +363,68 @@ describe('User', function() { | ||
| }); | ||
| }); | ||
| describe('Password length validation', function() { | ||
| var pass72Char = new Array(70).join('a') + '012'; | ||
| var pass73Char = pass72Char + '3'; | ||
| var passTooLong = pass72Char + 'WXYZ1234'; | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be consistent with the other 2. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. basically we need the first 72 chars to be identical with passTooLong, and Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for something different starting at the 73rd index so we can show how the test fails. varpass72Char=newArray(70).join('a')+'012';varpass73Char=pass72Char+'3';varpassTooLong=pass73Char+'456789'; | ||
| it('rejects passwords longer than 72 characters', function(done) { | ||
| try { | ||
| User.create({ email: 'b@c.com', password: pass73Char }, function(err) { | ||
| if (err) return done (err); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we decide to keep the current implementation, where invalid passwords throw an error, then in this test you should call done(newError('User.create() should have thrown an error.')); | ||
| done(new Error('User.create() should have thrown an error.')); | ||
| }); | ||
| } catch (e) { | ||
| expect(e).to.match(/Password too long/); | ||
| ||
| done(); | ||
| } | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Something is still weird here. If we expect error in the callback API, why do we need try catch. We talked about this over hangout last time @loay. IMO, we should be try/catching before we return the response to the user (they should only have to handle the @bajtos Thoughts?
| ||
| }); | ||
| it('rejects a new user with password longer than 72 characters', function(done) { | ||
| try { | ||
| var u = new User({ username: 'foo', password: pass73Char }); | ||
| assert(false, 'Error should have been thrown'); | ||
| } catch (e) { | ||
| expect(e).to.match(/Password too long/); | ||
| done(); | ||
| } | ||
| }); | ||
| it('accepts passwords that are exactly 72 characters long', function(done) { | ||
| User.create({ email: 'b@c.com', password: pass72Char }, function(err, user) { | ||
| if (err) return done(err); | ||
| User.findById(user.id, function(err, userFound) { | ||
| if (err) return done (err); | ||
| assert(userFound); | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
| it('allows login with password exactly 72 characters long', function(done) { | ||
| User.create({ email: 'b@c.com', password: pass72Char }, function(err) { | ||
| if (err) return done(err); | ||
| User.login({ email: 'b@c.com', password: pass72Char }, function(err, accessToken) { | ||
| if (err) return done(err); | ||
| assertGoodToken(accessToken); | ||
| assert(accessToken.id); | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
| it('rejects password reset when password is more than 72 chars', function(done) { | ||
| User.create({ email: 'b@c.com', password: pass72Char }, function(err) { | ||
| if (err) return done (err); | ||
| User.resetPassword({ email: 'b@c.com', password: pass73Char }, function(err) { | ||
| assert(err); | ||
| expect(err).to.match(/Password too long/); | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| describe('Access-hook for queries with email NOT case-sensitive', function() { | ||
| it('Should not throw an error if the query does not contain {where: }', function(done) { | ||
| User.find({}, function(err) { | ||
| @@ -678,6 +740,23 @@ describe('User', function() { | ||
| done(); | ||
| }); | ||
| }); | ||
| it('allows login with password too long but created in old LB version', | ||
| function(done) { | ||
| var bcrypt = require('bcryptjs'); | ||
| var longPassword = new Array(80).join('a'); | ||
| var oldHash = bcrypt.hashSync(longPassword, bcrypt.genSaltSync(1)); | ||
| User.create({ email: 'b@c.com', password: oldHash }, function(err) { | ||
| if (err) return done(err); | ||
| User.login({ email: 'b@c.com', password: longPassword }, function(err, accessToken) { | ||
| if (err) return done(err); | ||
| assert(accessToken.id); | ||
| // we are logged in, the test passed | ||
Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please assert the user is logged in instead of adding a comment (ie. verify the token, etc). | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| function assertGoodToken(accessToken) { | ||
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 have to use
options.passwordbecause otherwiselengthwill not be defined and will break few other test cases and cause errors. I meanlengthin mainvalidatePasswordfunction.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.
Got it. The new password is passed in as
options.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 don't think that's true.
resetMethoddoes not touch the password, it only creates an access token. See also the API docs.Let's not worry about that right now and fix it as part of #382.
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.
Here is the documentation explaining how to implement password reset: https://github.com/strongloop/loopback-example-user-management#how-do-you-perform-a-password-reset-for-a-registered-user
Gist - the password is updated via
user.updateAttribute, which callsuser.updateAttributesunder the hood IIRC.