DEV Community

Jase
Jase

Posted on

Creating a Simple Authentication System Using BCryptJS, JWT, and Express-Validator

Introduction

Many modern websites require users to create an account in order to take full advantage of their services. While this can be convenient depending on the platform, it also leaves users vulnerable to one of the ever-enduring problems of the Internet — cyberattacks. Fortunately, there are many methods available to protect user data from theft. BCryptJS and JSONWebToken are two of the most commonly used libraries for this purpose. BCrypt, to put it simply, is a function that obscures passwords to make them harder for attackers to decipher. JSONWebToken (JWT) is a library that creates a token that can be used to authenticate users without requiring them to log in again. During authentication, JWT can check to ensure that a token is valid and proceed accordingly. This tutorial will demonstrate how to use BCrypt's JavaScript library in conjunction with JWT and Express-Validator.

Prerequisites

For the purpose of this tutorial, it is assumed that you have a working ExpressJS backend. This tutorial will also be using POSTGreSQL's PG library to store user data, though you are not required to use it. Simply replace any query calls in this tutorial with ones from the appropriate library for your database.

In order to install BCryptJS, Express-Validator, and JWT, use the following command:

`npm install bcryptjs express-validator jsonwebtoken`
Enter fullscreen mode Exit fullscreen mode

Salting and Hashing

Before we begin, it's important to understand what "salting" and "hashing" actually mean. Salting is the process BCrypt uses to assign a random, unique value prior to hashing. This is done to ensure that two identical passwords will have different hashes. Hashing is the process used to convert the password itself to an assorted string of characters.

To start, create two middlewares: one for creating an account, and another for logging into an account.

In your account creation middleware, use Express Validator's body method to set the validation for your user data. To keep it simple, we're going to validate only usernames and passwords. Be sure to keep your validators at the top of your function!

exports.create_account = [
body('username', 'Please enter a username.').trim().custom(async user => {
const account = await db.query(
SELECT * FROM users WHERE username = $1`,
[user]
);

        if(account.rows.length !== 0) {
            return await Promise.reject('This username is already in use.');
        }
    }),
    body('password', 'Please enter a password.').notEmpty().custom(async password => {
        if(password.length < 8) {
            return await Promise.reject('Your password is too short.');
        }
    }),
    body('confirm', 'Please re-enter your password.').notEmpty().isLength({min: 8}).custom(async (confirm, {req}) => {
        if(confirm !== req.body.password) {
            return await Promise.reject('The passwords do not match.');
        }
    }),

    (req, res) => {
        ...
    }
]`
Enter fullscreen mode Exit fullscreen mode

Don't forget to use async/await when using Express-Validator's custom method, as the function needs time to run. Express-Validator also has many more methods that can be used to handle user inputs. Please consult the documentation for this library for further information.

After this, we can write the callback function for our middleware. If you want to retrieve any errors from the above validators, you need to include the validationResult method from Express Validator. Also, to prevent BCryptJS from running unnecessarily, we're going to put the remainder of this function in a conditional.

`const errors = validationResult(req);

if(!errors.isEmpty()) {
    res.status(400).json({errors: errors});
}
else {
    ...
}
`
Enter fullscreen mode Exit fullscreen mode

Now that we've included our validators and error handling, we can write the code to both salt and hash a user's passwords, and to create a token. While BCryptJS gives us the option to use the getSalt method, we're going to include the salt as a parameter for the hash method. To maintain optimal performance, the salt value will be 10. If you want to use a larger number, you may do so; however, be mindful that hashes will take longer to validate.

`bcrypt.hash(req.body.password, 10, async (err, hashWord) => {
    if(err) {
        return res.status(500).json({server_error: err});
    }
    else {
        const user = await db.query(
            `INSERT INTO users (username, password) VALUES ($1, $2)`,
            [req.body.username, hashWord]
        );

        jwt.sign({id: user.rows[0].id}, process.env.TOKEN_KEY, {expiresIn: '1h'}, 
            (err, key) =>  {
                if(err) {
                    return res.status(500).json({server_err: err});
                }
                else {
                    res.cookie('usertoken', key, {
                        expires: new Date(Date.now() + 3600000),
                        secure: false,
                        httpOnly: true,
                        path: '/api'
                    }).status(201).redirect('/api/home');
                }
            });
    }
});
Enter fullscreen mode Exit fullscreen mode

`
Before going any further, there are some things that you need to be mindful of:

1) For security reasons, it is highly recommended to use only the user's id when creating the token. This helps prevent sensitive data, like passwords and debit card numbers, from being accessed.

2) When building your project, it's fine to have secure set to false. However, for deployment, the value must be changed to true.

3) The value of TOKEN_KEY may be set to whatever you like. Its purpose is simply to validate the token being read by your code.
It's highly recommended that this key be kept in a .env file, and for that file to be added to .gitignore.

If you'd like, you can run your app and send input data to this endpoint. Once it's been submitted, check your database. If the password has been saved as a string of random characters, then it's successfully been hashed. Also, don't forget to check your headers using either the console or the Application tab of your browser's dev tools. If you see "usertoken" accompanied by a string of characters, then your token has successfully been created.

Now, we can move on to validating the hash.

Validating the Hash

Since our login middleware will only be checking to see if the input data matches saved user data, we're going to use only BCryptJS and JWT for this portion.

Before we can validate the hash, we need to write the code for error handling. Like in the account creation middleware, we're going to wrap everything in conditionals.

`exports.log_into_account = async (req, res) => {
    const user = await db.query(`SELECT * FROM users WHERE username = $1`, [req.body.username]);

    if(user.rows.length === 0) {
        return res.status(400).json({user_error: 'No account with this username exists.'});
    }
    else {
        if(req.body.password.length === 0) {
            return res.status(400).json({pass_error: 'Please enter a password.'});
        } 
        else {
            ...
        }
    }
}`
Enter fullscreen mode Exit fullscreen mode

We can now write the code to validate user credentials using BCrypt's compare method. This method simply compares the given password with the hashed password for the given user. If there's a match, then we can use a callback function to provide authentication to the user.

`bcrypt.compare(req.body.password, user.rows[0].password, (err, result) => {
    if(err) {
        return res.status(500).json({server_err: err});
    }
    else if(result) {
        jwt.sign({id: user.rows[0].id}, process.env.TOKEN_KEY, {expiresIn: '1h'}, 
        (err, key) => {
            if(err) {
                return res.status(500).json({error: err});
            }
            else {
                res.cookie('usertoken', key, {
                    expires: new Date(Date.now() + 3600000),
                    secure: false,
                    httpOnly: true,
                    path: '/api'
                }).sendStatus(200);
            }
        });
    }
    else {
        return res.status(400).json({pass_err: 'Your password is incorrect.'});
    }
});`
Enter fullscreen mode Exit fullscreen mode

In your app, enter the information for the account that you created earlier. If you have not done so, then do it now, and then log into the account. Once again, be sure to check your headers for the token.

Conclusion

BCryptJS is a crucial part of modern web applications despite its simplicity. When using it in conjunction with JWT and Express-Validator, you can create an effective system for creating and authenticating user data. From there, you may want to look into creating middleware that can verify tokens to ensure that only authenticated users can access certain parts of your application.

Top comments (0)