Getting started with #SailsJS - Part 2 - Validations and password hashing
In the previous post we created a User collection using the in-built sails generators. This allows you to create, update, delete users. Note that currently the password is being stored in plain text which is something that we want to avoid. We need to hash the passwords before they are stored in the database.
1. Password hashing
To do this, we have some model lifecycle callbacks that we can override and change the behavior. We will use the beforeCreate callback which is called before a new model object is created. Add the following code in the User model:
/* Lifecycle Callbacks */
beforeCreate: function(values, cb) {
if(!values.password || !values.confirmation || values.password != values.confirmation) {
return cb({err: ["Password does not match confirmation"]});
}
// Hash password
bcrypt.hash(values.password, 10, function(err, hash) {
if (err) return cb(err);
values.encryptedPassword = hash;
//Delete the passwords so that they are not stored in the DB
delete values.password;
delete values.confirmation;
//calling cb() with an argument returns an error. Useful for canceling the entire operation if some criteria fails.
cb();
});
}
In the request, we are passing the password and confirmation keys which will be validated in the server. An error is returned if the password and confirmation password do not match. If the input is fine, we will create a hash from the password and store it as the encrypted password. We are using the bcrypt library for hashing and comparing the passwords from the request.
By default, the response to the user created contains the encrypted password which we do not want to return. To prevent this, we can override the toJSON function which serializes the response and delete the encryptedPassword before sending the JSON response. Add the following code in the User model:
attributes: {
...
toJSON: function() {
var obj = this.toObject();
delete obj.encryptedPassword;
return obj;
}
}
The CRUD operations on the /user collection are currently publicly accessible. Having this API publicly accessible is very dangerous as this will allow any user to delete or update the user information. Sails by default enables, the REST routes, actions and shortcut APIs to help speed up development. We will need to disable this and write two routes:
- Sign up
- Login
All other routes for the /user should be disabled.
To do this, we will first disable shortcut routes as this is not recommended to be used in production. To do this, we need to set the shortcuts attribute in the project-folder/config/blueprints.js to false. This will disable the shortcuts API for all the controllers in the app.
Next we need to disable the REST routes for the UserController. To do this, add the following config in the UserContoller:
_config: {
actions: true,
shortcuts: false,
rest: false
}
This will disable all the routes for the user collection and allow only the actions defined in the controller.
2. Implementing signup and login actions
Next we need to implement the signup and login actions in the UserController. Add the following code in the UserController:
'signup': function(req, res) {
User.create(req.body).exec(function(err, user) {
if (err) {
return res.serverError(err);
}
return res.json(user);
});
},
'login': function(req, res) {
//Return error if email or password are not passed
if (!req.body.email || !req.body.password) {
return res.badRequest({
err: "Email or password cannot be empty"
});
}
//Find the user from email
User.findOne({
email: req.body.email
}).exec(function(err, user) {
if (err) {
return res.serverError(err);
}
if (!user) {
return res.notFound({err: 'Could not find email,' + req.body.email + ' sorry.'});
}
//Compare the password
bcrypt.compare(req.body.password, user.encryptedPassword, function(err, result) {
if(result) {
//password is a match
return res.json(user);
} else {
//password is not a match
return res.forbidden({err: 'Email and password combination do not match'});
}
});
});
}
Here we have added a few validations to check the input data that the client sends and appropriate error is returned incase it is not in the expected format.
All the code that we have discussed is present in the git repo.