Every year we continue to see news articles and Pastebins about data breaches where user accounts were stored either in plaintext (seriously!) or using an inferior hashing algorithm. This practice left thousands and sometimes millions of users vulnerable, not only on the original site in question, but on any additional sites on which the same credentials were used.
When working with node.js applications, whether a vanilla Javascript application, a web server like express.js or connect or anything in between, there’s a secure way we can store user credentials, but it’s not just using the right hashing algorithm. I’ve talked extensively about how the right tool also includes the necessary ingredient of “time” as part of the password hashing algorithm and what it means for the attacker who’s trying to crack passwords through brute-force.
In short, while strong hashing algorithms are important, password crackers can generate an enormous amount of password-guesses in order to find the right needle in the haystack. Cycling a user’s password through N number of hashing rounds forces a cracker to also go through the same number of hashing rounds to get the same output. A supercomputer could have generated 63,000,000,000 tries per second, but when forcing a time constraint under the same algorithm, it can only generate 71,000.
We’ve looked at other password-based key derivation functions such as pbkdf2 in the past. Today, I want to introduce you to bcrypt and specifically the NPM (Node Package Manager) package you can use in our next node.js based web application.
Implementation
What I want to show you is more than just how you utilize bcrypt to provide cryptographic hashes of user passwords, but also what it provides out of the box in the way of future-proofing and an example how we can hook into the functionality and flow of a node.js application.
Installing
We can get started by installing the bcrypt package:
npm install —save —save-exact [email protected]
Wondering about the “—save-exact”? Unfortunately, the default behavior of installing a npm package is to prefix the package version in your package.json file with a caret (^)
e.g. “bcrypt”: “^0.8.7"
Either one of the NPM package version prefixes using the caret (^) or tilde (~) states that you blindly accept patches or minor updates to said package.
If you’re publishing NPM packages, blindly accepting updates to downstream dependencies is a dangerous proposition. This can potentially, cause a chain-reaction to consumers of your package when an error occurs in a dependency. But that’s not the problem I am thinking of. When working with any 3rd party software, especially security based software, blinding accepting updates that could potentially introduce security holes before you or the community has a chance to vet them could be lethal.
By using the —save-exact and specifying a particular version, we won’t inadvertently update that package when someone runs a npm update
.
Overview
Bcrypt’s API has a number of functions, but the two we are going to focus on and the main purposes of the package are to 1) generate cryptographically strong hashes of user passwords over the course of a number of hashing rounds and 2) perform a comparison of a submitted password guess.
Hashing
bcrypt.hash(password, rounds, callback);
The Hash API call follows the standard node.js asynchronous programming style, allowing you to pass in a call back. It also allows you to specify a number of rounds. However, these rounds aren’t exactly what you might envision based on what we have seen with other libraries (such as PBKDF2, where 1=1).
A round for bcrypt is actually 2^n. Therefore, 16.5 would actually be 92,681 rounds of hashing that would be performed (2^16.5 = 92,681).
But more importantly, there are two unseen actions that bcrypt takes into account.
- Using the async hash() function also generates the high level of entropy salt that will be used with the hashing of the password.
- The outcome hash contains the necessary external information such as the salt and round count necessary for comparison. What this means is that if at some point, there’s a need to dial-up the number of rounds of hashing, it won’t disturb existing user password hashes that were generated with a different number of hash rounds.
Comparing
bcrypt.compare(guess, actual, callback)
This should be close to self-explanatory. We can provide the password guess along with the correct value and a callback. Bcrypt will in turn, distinguish the rounds, salt and hash from the actual value and put the submitted password guess through the specified number of hashing rounds before finally comparing the results for a match.
Implementation
The above is all fine and dandy, except, where do we use it? Every application is different, including storage, middleware, possible inclusion of an ORM… the list goes on. Therefore, below is an example of using bcrypt to hash newly registered users’ passwords.
In this particular application, I’m using the object data modeling (ODM) tool Mongoose for building in validation layers for a MongoDB database collections. A mongoose schema is a definition of a collection object that will be saved in MongoDB. Mongoose provides a number of hooks that you can tie in. Here is an example of utilizing the save pre-hook function for firing off the hashing of a user’s password before saving the user.
Creating a New User
Below is a Mongoose schema called UserSchema which provides the definition of the fields we expect in a saved user document in MongoDB. We can define a middleware function to be called before the “save” function of a mongoose schema is ultimately called.
NOTE: The bcrypt.hashAsync
function is a result of promising the bcrypt module using bluebird’s promisfying function first:
import mongoose from "mongoose"; import Promise from "bluebird"; const bcrypt = Promise.promisifyAll(require("bcrypt")); UserSchema.pre("save", async function (next) { if (!this.isModified("password")) { return next(); } try { const hash = await bcrypt.hashAsync(this.password, 16.5); this.password = hash; next(); } catch (err) { next(err); } });
Here we are defining the middleware to run before (“pre”) the “save” function is called for a mongoose UserSchema object. In return, we will generate a hash of the user submitted password, and that value will be stored in the “password” field of the user document instead of the actual submitted plaintext password.
Below is a view of the actual defined UserSchema. Other than the referenced “password” field above in the pre “save” hook middleware, none of the schema directly affect what we’re doing here. I still wanted to show you for completion.
Mongoose Schema
const UserSchema = new Schema({ firstName: String, lastName: String, username: { type: String, index: { unique: true } }, password: { type: String, required: true, match: /(?=.*[a-zA-Z])(?=.*[0-9]+).*/, minlength: 12 }, email: { type: String, require: true, match: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i }, created: { type: Date, required: true, default: new Date() } });
Therefore, when creating a new user such as the following block, and calling save on the UserSchema instance (User), the predefined middleware that we defined will be called to hash the new user’s submitted password.
NOTE: “User” is a returned mongoose model that is defined by a Mongoose schema that is directly bound to a specific database connection. I have left this out since it is irrelevant, but I want to show the mongoose “save” function being called that would trigger the above middleware that we defined.
const submittedUser = { firstName: firstName, lastName: lastName, username: email, email: email, password: password, created: Date.now() }; const user = new User(submittedUser); await user.save() .then(function (doc) { if (doc) { //log user creation } }) .catch(function (err) { //handle error });
User Login (Comparing)
Finally, in the case where we have a user logging in or re-authenticating when accessing a key access area of our application, we’re going to want to compare a submitted password guess to a user’s existing stored password hash.
Again, we’re using Mongoose as a hook into the process. Mongoose provides a way to define instance methods of its schemas, which we’ll use as an example for being able to validate a submitted password-guess to a valid user.
UserSchema.methods.passwordIsValid = function (password) { try { return bcrypt.compareAsync(password, this.password); } catch (err) { throw err; } };
Again, we’ve defined a promise-returned version of the async bcrypt method “compare” to return a promise to the caller. NOTE: That’s not required, and you can simply call bcrypt.compare() and provide a callback as a 3rd argument.
Upon finding a user:
const existingUser = await User.findById(id); if (await existingUser.passwordIsValid(password)){…}
This “existingUser” is an instance of the User Schema provided through the mongoose “User” model. We can call the “passwordIsValid” instance method that’s defined above, which will return a promise that, when resolved, will provide a boolean of whether it was a match.
Conclusion
As hardware gets faster, the need to raise the hashing rounds proverbial bar increases. We can do so without jeopardizing the current existing users.
Bcrypt doesn’t have to be utilized in this fashion either, nor is it a module for use in strictly web-based applications. But it is a well-vetted, maintained and widely distributed NPM package that can be used to securely store our users’ password hashes.
In a future post, we’ll look at how we can utilize some other NPM packages to further safeguard our user’s accounts, specifically, the highly overlooked ingredient of our user’s password composition.
Node.js and Password Storage with Bcrypt also published on Summa.com