Custom Development

Securing Node.js: User Account Password Requirements in Express.js

Max McCarty

This post was also published on LockMeDown.com, a security-focused blog for developers written by Max McCarty. 

If you’ve done a good share of web development, there’s a likelihood you have implemented some type of password requirements for new user registration, enforcing certain parameters on the passwords users submit.  But where did those requirements come from?  What forms have you come across over time?  What you think are good password requirements?  What we’re actually talking about is password composition. Building out these requirements in a node.js web application isn’t any different.

Password Composition


So, what is “good” password composition?  

Is it this?

password-min.jpg

Maybe it’s this?

password-alphanumerica.jpg

Actually, it’s all of these and more.  I’ve talked about good password composition extensively in an article on securing sensitive data, so I won’t go into any elaborate detail.  But there are two crucial points to remember::

  1. All stored password hashes are basically a needle in a haystack.  
  2. The key is  how long it takes to find that needle.

You’ve probably heard a lot about different kinds of password requirementsspecial characters, combinations of alphanumeric and upper and lowercase letters.  All of these requirements are important and help widen the spectrum of possible passwords.  But do you know what single component will have the biggest impact on the security of a password? Surprisingly, it’s length. That’s why, instead of passwords, I advocate for using passphrases.

You can get a good idea of how it plays into the overall ability to brute force password cracking using the Gibson Research Space Calculator. For example, take the following two password compositions and see how they line up:


Password

Length

Alphanumeric

Upper & Lower

Special

Massive Cracking Array Scenario

Leetzsp3k!

10

YES

YES

YES

1 Week

no soup for you

15

NO

NO

NO

1,000 Centuries



Yes, you’re reading that correctly.  Using an offline, superpower array of GPU’s to brute force crack the second password will take over 1,000 centuries.  In contrast, the first password, which has 10 alphanumeric, upper and lowercase letters and special characters,only takes 1 week to crack.

So how do we enforce the proper password requirements in a node.js web application?


Express Validator

I’m going to show you a two-layer approach to enforcing password requirements—one that lets us validate a new user-submitted account password upon submission, and do a second validation check when saving to a MongoDB database.

Obviously, your node.js web server of choice might not be Express, but the NPM module we’re going to utilize is a wrapper to the heavily utilized NPM validator module, which is not based on any node.js framework.  Express-validator provides a number of convenient methods off of the express request object such as checking strictly the body, query and parameters, or all of them.  It also provides the ability to define schema’s that leverage the underlying validation methods that validator.js provides along with the ability to provide custom validation methods.

WARNING: For the ease of following along, I have stuffed the logic in the route rather than taking the correct, pragmatic approach of separating it out.

Imagine we have the following express route when a new user attempts to register:

authenticationRouter.route("/api/user/register")

  .post(cors(), async function (req, res) {

      try {

          const User = await getUserModel();

          const{email, password, firstName, lastName} = req.body;

          const existingUser = await User.findOne({username: email}).exec();

          if (existingUser) {

              return res.status(409).send(`The specified email ${email} address already exists.`);

          }

          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 (user) {

                  if (user) {

                      console.log(colors.yellow(`Created User ${JSON.stringify(user)}`));

                  }

              })

              .catch(function (err) {

                  if (err) {

                      console.log(colors.yellow(`Error occurred saving User ${err}`));

                  }

              });

          res.status(201).json({user: {firstName: user.firstName, lastName: user.lastName, email: user.email}});

      } catch (err) {

          throw err;

      }

  });

If you’re observant, you’ll notice that we are blatantly accepting values the user is submitting, such as the email field. If the user doesn’t exist, we’ll proceed to simply accept the password they provide.


Let’s do something about this by implementing a schema that will define what values are acceptable.


We can create a file validationSchema.js


export const registrationSchema = {

      "email": {

          notEmpty: true,

          isEmail: {

              errorMessage: "Invalid Email"

          }

      },

      "password": {

          notEmpty: true,

          isLength: {

              options: [{ min: 12}],

              errorMessage: "Must be at least 12 characters"

          },

          matches: {

              options: ["(?=.*[a-zA-Z])(?=.*[0-9]+).*", "g"],

              errorMessage: "Password must be alphanumeric."

          },

          errorMessage: "Invalid password"

      }

};

Here we have defined an object that sets the rules for two other objects: “email” and “password”.   

email:

  • can’t be empty
  • must confirm to the validator.js rules of a valid email

password:

  • can’t be empty
  • must have a minimum of 12 characters
  • and must be alphanumeric

We have also specified field specific error messages to easily provide feedback to our user on the front end when their submitted email or password doesn’t conform.

Now, we can put this schema to use back in our authentication route for registering a new user:

import {registrationSchema}         from "../validation/validationSchemas”;

...

authenticationRouter.route("/api/user/register")

  .post(cors(), async function (req, res) {

      try {

          const User = await getUserModel();

          req.checkBody(registrationSchema);

          const errors = req.validationErrors();

          if (errors) {

              return res.status(500).json(errors);

          }

          const {email, password, firstName, lastName} = req.body;

          const existingUser = await User.findOne({username: email}).exec();

          if (existingUser) {

              return res.status(409).send(`The specified email ${email} address already exists.`);

          }

          const submittedUser = {

              firstName: firstName,

              lastName: lastName,

              username: email,

              email: email,

              password: password,

              created: Date.now()

          };

          const user = new User(submittedUser);

          await user.save();

          res.status(201).json({user: {firstName: user.firstName, lastName: user.lastName, email: user.email}});

      } catch (err) {

          res.status(500).send("There was an error creating user.  Please try again later");

      }

  });

After importing the registrationSchema object we created in the validationSchema.js file, we updated the route to call the checkBody() method off of the request and pass it the registrationSchema object.

Express-validatior will now only look for “email” and “password” properties on the request body and if they exist, will proceed to apply the rules that we defined in the schema for each of these fields.

If there are errors, when we call the validationErrors() method off of the request object req.validationErrors(), we can acquire any errors that were found with the submitted values on the request.  

But like any security, multiple layers can help us in the case that a mitigation breaks down.

Multi-layer Security with Mongoose and MongoDB

If you’re not working with MongoDB or the Object Data Modeling tool Mongoose, you can still use the following as a guide for implementing a database validation layer in whatever database you’re working with.

If you have worked with Mongoose before, you’re familiar with defining schemas and defining the shape of the data you’re saving to a MongoDB database.  Take for instance our User 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()

  }

});


I have reduced noise by only showing validation rules for the fields we were concentrating on password and email.  But we can see here that we are implementing the same rules we were enforcing on the form fields that were being submitted by the user when registering.

Then, back in our new user registration route:

authenticationRouter.route("/api/user/register")

  .post(cors(), async function (req, res) {

      try {

         
           //…removed for brevity

          const user = new User(submittedUser);

          await user.save();

          res.status(201).json({user: {firstName: user.firstName, lastName: user.lastName, email: user.email}});

      } catch (err) {

          res.status(500).send("There was an error creating user.  Please try again later");

      }

  });

When we call user.save() mongoose will enforce our schema rules we defined above and will throw an error if they don’t conform. In this way, we have yet one more place of validation before we push this data into our backend storage.

In a future post regarding access controls, we’ll see how we can utilize a routing hook to move validation checks such as these to a point that’s specific to a route, yet further way from our critical systems, such as a database.

 

Max McCarty
ABOUT THE AUTHOR

Max McCarty is a Senior Technical Consultant at Summa with a passion for breathing life into big ideas. He is the founder and owner of LockMeDown.com and host of the popular Lock Me Down podcast. As a software engineer, Max’s focus is on software security, and strongly believes in empowering the everyday developer with the information to write more secure software. When he’s not building new applications or writing about web security, you’ll find Max burning calories with his kids and spending time with his wonderful family. He’s also a serious history buff.