From d8aa6bdf0000278863434db298df149e5af3fcaa Mon Sep 17 00:00:00 2001 From: Loay Date: Wed, 3 Aug 2016 19:01:33 -0400 Subject: [PATCH] Add bcrypt validation https://github.com/strongloop/loopback/pull/2580 --- common/models/user.js | 24 +++++++++---- index.js | 2 +- test/user.test.js | 79 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 7 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 90a17c56..b857e38e 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -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 @@ -538,7 +538,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')); @@ -548,7 +547,14 @@ module.exports = function(User) { return cb.promise; } - UserModel.findOne({ where: {email: options.email} }, function(err, user) { + 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); } @@ -586,14 +592,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)); + 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. */ diff --git a/index.js b/index.js index ac439167..0f02a1c1 100644 --- a/index.js +++ b/index.js @@ -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' }); /** * loopback ~ public api diff --git a/test/user.test.js b/test/user.test.js index cfb7232e..0ac361bf 100644 --- a/test/user.test.js +++ b/test/user.test.js @@ -377,6 +377,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'; + + 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); + done(new Error('User.create() should have thrown an error.')); + }); + } catch (e) { + expect(e).to.match(/Password too long/); + done(); + } + }); + + 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) { @@ -692,6 +754,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 + done(); + }); + }); + }); }); function assertGoodToken(accessToken) {