Adding a secure password
Now that we’ve defined validations for the name and email fields, we’re ready to add the last of the basic User attributes: a secure password. The method is to require each user to have a password (with a password confirmation), and then store a hashed version of the password in the database.
A hashed password
The secure password machinery will be implemented using a beforeCreate
, beforeUpdate
method and bcrypt
module
app/models/user.js
var Sequelize = require('sequelize');
var sequelize = CONFIG.database;
var bcrypt = require('bcrypt');
var User = sequelize.define('user', {
name: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: true,
len: [1,50]
}
},
email: {
type: Sequelize.STRING,
allowNull: false,
validate: {
isEmail: true,
notEmpty: true,
len: [1,255]
}
},
password_digest: {
type: Sequelize.STRING,
validate: {
notEmpty: true
}
},
password: {
type: Sequelize.VIRTUAL,
allowNull: false,
validate: {
notEmpty: true
}
},
password_confirmation: {
type: Sequelize.VIRTUAL
}
}, {
freezeTableName: true,
indexes: [{unique: true, fields: ['email']}],
instanceMethods: {
authenticate: function(value) {
if (bcrypt.compareSync(value, this.password_digest))
return this;
else
return false;
}
}
});
var hasSecurePassword = function(user, options, callback) {
if (user.password != user.password_confirmation) {
throw new Error("Password confirmation doesn't match Password");
}
bcrypt.hash(user.get('password'), 10, function(err, hash) {
if (err) return callback(err);
user.set('password_digest', hash);
return callback(null, options);
});
};
User.beforeCreate(function(user, options, callback) {
user.email = user.email.toLowerCase();
if (user.password)
hasSecurePassword(user, options, callback);
else
return callback(null, options);
})
User.beforeUpdate(function(user, options, callback) {
user.email = user.email.toLowerCase();
if (user.password)
hasSecurePassword(user, options, callback);
else
return callback(null, options);
})
module.exports = User;
To implement the data model, we first generate an appropriate migration for the password_digest
column.
~/sample_app $ sequelize migration:create --name add_password_digest_to_users
Loaded configuration file "config/database.json".
Using environment "development".
Using gulpfile /usr/lib/node_modules/sequelize-cli/lib/gulpfile.js
Successfully created migrations folder at "/home/train/projects/node_projects/workspace/sample_app/db/migrate".
New migration was created at /home/train/projects/node_projects/workspace/sample_app/db/migrate/20160128145145-add_password_digest_to_users.js .
Finished 'migration:create' after 29 ms
We use the addColumn
method to add a password_digest
column to the user
table.
db/migrate/[timestamp]-add_password_to_users.js
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.addColumn(
'user',
'password_digest',
Sequelize.STRING
)
},
down: function (queryInterface, Sequelize) {
}
};
To apply it, we just migrate the database
~/sample_app $ sequelize db:migrate
Loaded configuration file "config/database.json".
Using environment "development".
Using gulpfile /usr/lib/node_modules/sequelize-cli/lib/gulpfile.js
Starting 'db:migrate'...
Finished 'db:migrate' after 647 ms
== 20160128145145-add_password_digest_to_users: migrating =======
== 20160128145145-add_password_digest_to_users: migrated (0.643s)
To make the password digest, we use a state-of-the-art hash function called bcrypt
~/sample_app $ npm install bcrypt --save
User has secure password
The tests are now failing, as you can confirm at the command line
~/sample_app $ mocha test/models/user_test.js
UserTest
Unhandled rejection AssertionError: { [SequelizeValidationError: notNull Violation: password cannot be null]
...
To get the test suite passing again, we just need to add a password and its confirmation
test/models/user_test.js
require('trainjs').initServer();
var assert = require('assert');
describe('UserTest', function () {
var user;
beforeEach(function() {
user = User.build({
name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar"
});
});
...
});
Now the tests should be successful
~/sample_app $ mocha test/models/user_test.js
UserTest
✓ should be valid
✓ name should be present
✓ email should be present
...
16 passing (352ms)
Minimum password standards
It’s good practice in general to enforce some minimum standards on passwords to make them harder to guess. Picking a length of 6 as a reasonable minimum leads to the validation test
test/models/user_test.js
require('trainjs').initServer();
var assert = require('assert');
describe('UserTest', function () {
var user;
beforeEach(function() {
user = User.build({
name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar"
});
});
...
it('password should be present (nonblank)', function(done) {
user.password = user.password_confirmation = " ".repeat(6);
user.validate().then(function(errors){
assert.notEqual(errors, undefined);
done();
});
});
it('password should have a minimum length', function(done) {
user.password = user.password_confirmation = " ".repeat(5);
user.validate().then(function(errors){
assert.notEqual(errors, undefined);
done();
});
});
});
You may be able to guess the code for enforcing a minimum length constraint by referring to the corresponding maximum validation for the user’s name
app/models/user.js
var Sequelize = require('sequelize');
var sequelize = CONFIG.database;
var bcrypt = require('bcrypt');
var User = sequelize.define('user', {
...
password: {
type: Sequelize.VIRTUAL,
allowNull: false,
validate: {
notEmpty: true,
len: [6, Infinity]
}
},
password_confirmation: {
type: Sequelize.VIRTUAL
}
}, {
freezeTableName: true,
indexes: [{unique: true, fields: ['email']}],
instanceMethods: {
authenticate: function(value) {
if (bcrypt.compareSync(value, this.password_digest))
return this;
else
return false;
}
}
});
...
At this point, the tests should be successful
~/sample_app $ mocha test/models/user_test.js
UserTest
✓ should be valid
✓ name should be present
✓ email should be present
...
18 passing (260ms)
Creating and authenticating a user
Now that the basic User model is complete, we’ll create a user in the database as preparation for making a page to show the user’s information in “Showing users” Section.
> require('trainjs').initServer()
> User.create({name: "Dang Thanh", email: "[email protected]", password: "foobar", password_confirmation: "foobar"})
Executing (default): INSERT INTO `user` (`id`,`name`,`email`,`password_digest`,`updatedAt`,`createdAt`) VALUES (NULL,'Dang Thanh','[email protected]','$2a$10$7gVwYayQoKFSEkJfobhAne4EjC52Djt7x1cl9cponFtAn.zVtfP0e','2016-01-29 05:30:00.511 +00:00','2016-01-29 05:30:00.511 +00:00');
> var user;
> User.findOne({ where: { email: "[email protected]" } }).then(function(data) { user = data })
> user.password_digest
'$2a$10$7gVwYayQoKFSEkJfobhAne4EjC52Djt7x1cl9cponFtAn.zVtfP0e'
The authenticate
method in model determines if a given password is valid for a particular user by computing its digest and comparing the result to password_digest in the database. In the case of the user we just created, we can try a couple of invalid passwords as follows:
> user.authenticate('not_the_right_password')
false
> user.authenticate('foobaz')
false
Here user.authenticate returns false for invalid password. If we instead authenticate with the correct password, authenticate returns the user itself
> user.authenticate('foobar')
{ dataValues:
{ id: 1,
name: 'Dang Thanh',
email: '[email protected]',
password_digest: '$2a$10$7gVwYayQoKFSEkJfobhAne4EjC52Djt7x1cl9cponFtAn.zVtfP0e',
createdAt: Fri Jan 29 2016 12:30:00 GMT+0700 (ICT),
updatedAt: Fri Jan 29 2016 12:30:00 GMT+0700 (ICT) },
...