Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone's Guide to

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

Deciding what type of password security you should implement for your users can be a more difficult decision than you think. Learn more about what could work best for you and your users right here.

· Security Zone
Free Resource

Discover how to provide active runtime protection for your web applications from known and unknown vulnerabilities including Remote Code Execution Attacks.

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 requirements—special 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 NPMvalidator 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.

Find out how Waratek’s award-winning application security platform can improve the security of your new and legacy applications and platforms with no false positives, code changes or slowing your application.

Topics:
registration ,user experience ,security ,password security

Published at DZone with permission of Max McCarty, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}