{ Password Hashing with bcrypt. }

Objectives:

By the end of this chapter, you should be able to:

  • Define essential terms like one-way encryption, hashing, and salting
  • Explain how to securely store passwords in a database
  • Add authentication to an express app

Auth Intro / Terms

bcrypt - The library we will be using for hashing, salting, and decrypting passwords is the bcrypt library. The library is based on the blowfish cipher, a widely recognized algorithm for secure one way hashing.

one-way encryption - With one-way encryption, also called hashing, encryped information is not meant to be decrypted. This is quite common when thinking about passwords. Ideally the only person who should be aware of their password is the person who created it, as this is much more secure. As you'll see, we'll be hashing passwords and storing the hashes in a database. When a user then attempts to log in, we can hash the password they provide and see if there is a match between hashes. This allows us to authenticate a user without knowing what their password is. While this is a good start, hashing alone is not secure as it opens yourself up to dictionary attacks.

two-way encryption - In contrast with one way encryption, two-way encryption is meant to be decrypted, often with some sort of key that the parties can use for decryption. This is very common when establishing a secure connection between another party (SSH keys on GitHub).

dictionary attack - A dictionary attack occurs when a hacker takes all possible combinations of characters and runs it through a hashing algorithm. Once they have found a hash which matches yours, they can look up the text in the dictionary they have built and voila - you are hacked!

salting - In order to prevent dictionary attacks, we add a randomized string of characters to the password (the salt) and then hash the entire password. This prevents someone from successfully creating a dictionary.

Auth with bcrypt

To get started with bcrypt, let's first install the module using npm install --save bcrypt. Instead of building a new express app, let's start by playing around in a node console, or create a file and add the following:

var bcrypt = require("bcrypt");
var password = 'secret';
var dbPassword;
var saltRounds = 10;

bcrypt.hash(password, saltRounds).then(function(hashedPassword) {
  console.log("hash", hashedPassword);
  return hashedPassword; // notice that all of these methods are asynchronous!
}).then(function(hash) {
  return bcrypt.compare(password, hash); // what does this method return?
}).then(function(res) {
  console.log('match', res);
});

Let's step through this code a bit. First, bcrypt.hash will hash our password. We include a number (saltRounds), which you can roughly think of as measuring how many steps are involved in creating the hash. (Try changing 10 to 16, and see how much longer it takes to run the above code!)

Once the password is hashed, it is logged to the console. You should see something like this:

hash $2a$10$Ns876QMLlCV4nT5ctzDHJeRMrvbVvZeGHn3gtJ6sJn5fILfEivZGa

The bcrypt hash consists of four parts:

2a -> prefix
10 -> work factor
Ns876QMLlCV4nT5ctzDHJe -> salt
RMrvbVvZeGHn3gtJ6sJn5fILfEivZGa -> hashed password

The prefix just indicates that bcrypt was used to generate the string. The salt is a random string that is then combined with the password to generate the hashed password. In particular, the hash that we get from bcrypt includes the salt in it! This is what allows us to check passwords when users attempt to log in later: the combination of salt, work factor, and password uniquely determines the hash.

You should see match true get logged to the console, since we're checking the string 'secret' against the hash corresponding to that string. But if you try comparing hash to any other string, you should see that the console outputs match false instead.

Adding auth to our mongoose model

Now that we have an idea of how to create secure passwords and encrypt them, let's see what adding this information to a Mongoose schema would look like. Before we examine the code, we need to first understand how middleware in Mongoose works.

You may hear middleware being called pre or post "hooks" because they are functions that run before or after certain events. These events are:

init - When a document is initialized (before or after)
validate - When a document is validated (before or after)
save - When a document is saved (before or after)
remove - When a document is removed (before or after)

Let's see how we can add some pre-save middleware into our application so that when we save a user, we hash the password right before inserting it into the database.

var mongoose = require("mongoose");
var bcrypt = require("bcrypt");

var userSchema = new mongoose.Schema({
    username: {
        type: String,
        // usernames should always be required
        required: true,
        // and most importantly unique! We will need to be able to uniquely
        // identify a user when we discuss logging in
        unique: true
    },
    password: {
        type: String,
        // passwords do not have to be unique, but we really need them to exist
        required: true
    }
});

/* the callback function (2nd parameter below) accepts a parameter which we 
are calling "next". This is ALSO a callback function that needs to be executed 
when we would like to move on from this pre-save hook. If we do not invoke the 
"next" function, our server will hang. */
userSchema.pre('save', function(next){
    // we do not want to lose the correct context of the keyword `this`, so let's cache it in a variable called user
    var user = this;
    /* if the user's password has not been modified, do not continue with 
    hashing their password....this is for updating a user. If a user does not 
    update their password, do not create a new password hash! */
    if (!user.isModified('password')) return next();

    // if the user has modified their password, let's hash it
    bcrypt.hash(user.password, 10).then(function(hashedPassword) {
        // then let's set their password to not be the plain text one anymore, but the newly hashed password
        user.password = hashedPassword;
        // then we save the user in the db!
        next();
    }, function(err){
        // or we continue and pass in an error that has happened (which our express error handler will catch)
        return next(err);
    })
});

/* now let's write an instance method for all of our user documents which 
will  be used to compare a plain text password with a hashed password 
in the database. Notice the second parameter to this function is a 
callback function called "next". Just like the code above, we need 
to run next() to move on. */
userSchema.methods.comparePassword = function(candidatePassword, next) {
    // when this method is called, compare the plain text password with the password in the database.
    bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
        if(err) return next(err);
        // isMatch is a boolean which we will pass to our next function
        next(null, isMatch);
    });
};

var User = mongoose.model('User', userSchema);
module.exports = User;

It's also essential to remember that pre and post save hooks are not executed on update(), findOneAndUpdate(), etc, which means that when we update passwords we can not use these methods and instead need to first find the document, modify the document and then call the save method to properly trigger the pre save hook.

Including this logic in an application

Let's see what a login route might look like in an express application.

router.post('/login', function(req, res, next){
    // first - find a user by their username (which should always be unique)
    db.User.findOne({username: req.body.username}).then(function(user){
        // then check to see if their password is the same as the hashed one
        user.comparePassword(req.body.password, function(err, isMatch){
            if(isMatch){
                // if so - they are logged in!
                res.send('logged in!');
            } else {
                res.redirect('/users/login');
            }
        })
    }, function(err){
        res.send(err);
    })
});

Sample application

You can see a sample application with bcrypt here.

When you're ready, move on to Cookies and Sessions in Express

Continue