This post is Part 1 (of 2) on implementing secure username/password authentication for your Mongoose User models, originally posted on Jeremy Martin’s DevSmash Blog. In this first installment, we will discuss how to implement one-way encryption of user passwords with bcrypt, and how to subsequently use the encrypted password for login verification.
Update: Password Authentication with Mongoose (Part 2): Account Locking is now live!
Cast of Characters
From the Mongoose GitHub repo: “Mongoose is a MongoDB object modeling tool designed to work in an asynchronous environment.”
In other words, Mongoose provides a model layer for interacting with your MongoDB collections from Node. This model layer provides a common location for implementing document validation, persistence indirection, and other logic that should be abstracted from the business layer. Website: http://mongoosejs.com/
node.bcrypt.js is, well, bcrypt for Node. If you’re not familiar with bcrypt and why it’s a good thing, then I highly recommended Coda Hale’s excellent article on how to safely store a password. Website: https://github.com/ncb000gt/node.bcrypt.js/
Before we get into the code, let’s identify some objectives/requirements in our initial username/password authentication implementation:
The User model should fully encapsulate the password encryption and verification logic
The User model should ensure that the password is always encrypted before saving
The User model should be resistant to program logic errors, like double-encrypting the password on user updates bcrypt interactions should be performed asynchronously to avoid blocking the event loop (bcrypt also exposes a synchronous API)
Step 1: The User Model
Even if you aren’t too familiar with Mongoose schemas and models, the code below should be fairly easy to follow. It wouldn’t be a bad idea to read through some of the basicsthough. To start things off, let’s create our bare bones representation of a user; for the sake of this article, all we need is a username and a password:
Step 2: Password Hashing Middleware*
You might have noticed the unused
SALT_WORK_FACTOR references above - we’ll be using them in this step. As a quick refresher, the purpose of the salt is to defeat rainbow table attacks and to resist brute-force attacks in the event that someone has gained access to your database. bcrypt in particular uses a key setup phase that is derived from Blowfish.
For the purposes of this article, all you need to know about that is that the key setup phase is very computationally expensive, which is actually a good thing when trying to thwart brute-force attacks. How expensive depends on how many rounds or iterations the key setup phase uses - this is where our
SALT_WORK_FACTORcomes into play. The default used by node.bcrypt.js is 10, so I went ahead and made our explicit value the same.
Alright, let’s get back to the code.
The first thing we want to add to our User model is some Mongoose middleware that will automatically hash the password before it’s saved to the database. Here’s what that looks like:
The above code will accomplish our goal of always hashing the password when a document is saved to the database. There are a couple things to be aware of though: Because passwords are not hashed until the document is saved, be careful if you’re interacting with documents that were not retrieved from the database, as any passwords will still be in cleartext. Mongoose middleware is not invoked on
update() operations, so you must use a
save()if you want to update user passwords. Step 3: Password Verification Now that we have our User model and we’re hashing passwords, the only thing left is to implement password verification. Adding this to our model turns out to be just a few more lines of code:
Simple enough. Altogether Now Here’s what our User model looks like after adding our middleware and password verification method:
Sample Usage Assuming that you’ve saved the above code as
user-model.js, here’s how you would go about testing it:
“` Next Steps This post was just Part 1 on implementing secure username/password authentication for your Mongoose User models. Stay tuned for Part 2, in which we’ll discuss preventing brute-force attacks by enforcing a maximum number of failed login attempts. Thanks for reading!
Jeremy Martin is the creator of (recently launched) DevSmash.com, a software developer and Open Source Evangelist at his day job, a Node.js contributor, MongoDB fan boy, and husband to the greatest gal on the planet. Online he goes by @jmar777. Update: Password Authentication with Mongoose (Part 2): Account Locking is now live!