This material is now available in the Pluralsight.com course "Securing Your Node.js Web Application".Pluralsight Course Securing Your Node.js Web Application

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

Enforcing user password requirements in express.js

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 Express.js web application isn’t any different.

 

Password Composition

So, what is “good” password composition?  

 

Is it this?

Password requirements example: Express.js Website

 

Maybe it’s this?

 

Password requirements: Express.js Website

 

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.

 

The Needle in the Haystack

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 pass phrases.
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 such as in Express.js?

 

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/express.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.

 

Securing Node.js: User Account Password Requirements in Express.js” also published on Summa.com

About the author

Max McCarty

Max McCarty is a software developer 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.