DEV Community

Cover image for Setting up Auth Routes with Express
Ambadi Ritik
Ambadi Ritik

Posted on

Setting up Auth Routes with Express

In the previous part , we learnt how to connect to MongoDB with Mongoose. We also defined our basic User Schema.

In this part , we're going to setup our /login and our /registration routes. These API endpoints are going to allow our client-side applications to send POST requests to register new users as well as to allow existing users to login to their account.

Setting up our Routes ✨

Let's start by creating a new folder in our project directory called routes.

In this folder , we're going to create our Auth.route.js file. We're going to define all our Routes in this file and later import it in our server.js file.

const express = require('express');
const router = express.Router();

const User = require('../models/User.model');

router.post('/register',async(req,res) => {
  try {
   res.json({message:"This is the register route!"})
  }
  catch(err) {
   console.error(err.message)
  }
})

router.post('/login',async(req,res) => {
  try {
   res.json({message:"This is the login route!"})
  }
  catch(err) {
   console.error(err.message)
  }
})

In the code above , we require('express') and then instantiate Router which is built into Express.

Router helps us build out our routes. We can handle get , post , patch , delete and most other HTTP request methods for our routes by chaining the required type of request to our router object. i.e

router.get('/[our-required-route]',() => {
  //This is a callback function
})

We're going to define what should be done when the route is hit, inside the callback function.

In our first code snippet , our callback function is async. This will make our life easier later when we need to interface with MongoDB to fetch and post data.

For the sake of testing , we will send back json data , with a message key containing a string value using res.json.

The callback functions come with a req and res parameter which help us interact with a user's request and the response we can send back respectively.

Let's finally add this route to our server.js file. Add the following code before the require('./helpers/initDB')() line.

We will require our Auth.route file and initialise it to AuthRoute. We will finally use the AuthRoute by making use of the use method that Express provides. We'll also define the parent route to be /api/auth. This means that if we want to hit our register route , we'll actually have to hit /api/auth/register.

...

const AuthRoute = require('./routes/Auth.route');
app.use('/api/auth', AuthRoute);

...

Installing REST Client on VSCode to test our APIs ☀️

If you want to test the APIs we've just built , you can download the REST Client by going to the Extensions tab on VSCode. You can additionally download Postman or Insomnia as well to test your API.

Alt Text

Let's make a POST request to our APIs that we defined earlier.
In our routes folder, create a new file called route.http. Then write the following line into this file.

POST https://localhost:5000/api/auth/register

You'll see a Send Request label pop up just over this line now. Click on it.

This will now open a tab on the side with a JSON response.
This response should be

"message" : "This is the register route!"

Make sure your server is running before making the request. You can do this by using npm start.

Analysing our Login/Registration workflows

Before we can Login or Register users , we need to break down what we need to do step-by-step.

Let's look at our Registration workflow.

  1. Validate the received registration details
  2. If there is an error in received registration details , then return 400 status code and the error message.
  3. Check if email exists. (400 if error)
  4. Check is username exists. (400 if error)
  5. Salt and then hash the password. (Read Part One)
  6. Save our user in the database.

Let's break down our Login Workflow next.

  1. Validate the received login details.
  2. Check if user with given email exists. (400 if error)
  3. Check the received user password against the hashed DB password using bcrypt.compare().
  4. Return Success message if password matches , else return invalid details message. (Additionally provide a JWT Token which we will discuss in part 4)

In both the workflows described above , we need to validate the details that we receive from the client side. This involves a lot of string handling which can be tedious work.
However , in this tutorial we're going to use a ready-made package for validation called Joi.

We're also going to install another package called bcrpyt. Bcrypt provides ways to salt , hash and compare passwords with its built in methods.

Let's install them both. Exit your server using Ctrl+C or Cmd+C and run the following npm command.

npm install @hapi/joi bcrpyt

Writing our Joi validation schemas 🌈

Let's get started on writing our Joi validation schemas. Writing a Joi Validation schema is very easy. We define a Joi object and define the requirements that our data in this Joi object should have. We can do this by chaining together the built-in methods that Joi provides.

Want to check if a string has at least 6 characters and can only be alphanumerical?

We can achieve this simply with the following code

ourString: Joi.string().min(6).alphanum(),

Joi will return an error message if the ourString value does not pass the conditions.

Let's now go ahead and build out our validation schemas for the auth-api.

Create a validator.js file in your /helpers directory.
Add the following code to this file.

const Joi = require('@hapi/joi');

const registrationValidator = (data) => {
    const schema = Joi.object({
        username: Joi.string().min(6).required().alphanum(),
        email: Joi.string().min(6).required().email(),
        password: Joi.string().min(6).required(),
        role: Joi.string()
    })
    return schema.validate(data);
}

const loginValidator = (data) => {
    const schema = Joi.object({
        email: Joi.string().min(6).required(),
        password: Joi.string().min(6).required()
    })
    return schema.validate(data);
}

module.exports.registrationValidator = registrationValidator;
module.exports.loginValidator = loginValidator;

Finally let's require this file in our Auth.route.js file.

const { registrationValidator, loginValidator } = require('../helpers/validator');

Building our Register Route 🌈

Inside our try block , lets start by processing the data that we receive by using req.body.

try {
const { error } = registrationValidator(req.body);
if (error) {
return res.status(400).send(error.details[0].message);
}
}

We pass req.body to our registrationValidator function that we previously defined our validator.js file.

If our validator encounters an error in the receiver data , we're going to return the error message with a status code of 400.

You can test if the API works so far by going to the rest.http and adding the following

POST https://localhost:5000/api/auth/register
content-type: application/json
{
    "email":"test@test.com",
    "username":"test",
    "password":"test",
}

After hitting the Send Request button , you'll see that we get an error message with a 400 status code. This is because both our username and password are only 4 letters long.

Now that validation is done , We can check if the username or email already exist in the database.

Checking if username and email already exist

Add the following code next ,

//EmailExistCheck
const emailExists = await User.exists({ email: req.body.email });
if (emailExists) return res.status(400).send('Email already exists.');


//UsernameExistCheck
const userNameExists = await User.exists({ username: req.body.username });
if (userNameExists) return res.status(400).send('Username already exists.');

We use the exists method that MongoDB provides to check if a document containing the given data exists.

We'll return the error message with a 400 status code if either values exist.

Salting and hashing our passwords before storing

Let's make use of the bcrypt library that we had installed earlier. Make sure you've imported the bcrypt library with the following code.

const bcrypt = require('bcrypt');

Next , let's generate a salt using the in-built genSalt() method inside bcrypt.

   const salt = await bcrypt.genSalt(10);

If you're unaware about salting or hashing , read the first article of this series.

The bcrypt genSalt() method generates a salt for us that we'll now use with our password. Let's use the bcrypt.hash() method to hash our salted password. This method takes the base password and the generated salt as its parameters.

Go ahead and add the following code to your file next.

 const hashPassword = await bcrypt.hash(req.body.password, salt);

Now that we have hashed our password , let's go ahead and construct our new user object with the newly hashed password.

     const user = new User({
                username: req.body.username,
                email: req.body.email,
                password: hashPassword,
            });

Finally , let's save this user into our Database using the save() method.

   const savedUser = await user.save();
            res.send(savedUser);

Let's send back the user that we have saved just now as our response with the res.send() method.

Finally , go back to the rest.http file and make a POST request with three valid user credentials defined by our Joi Schema.

If everything went well , you should see the saved user's details containing the hashed password in your response.

You can additionally also go to your Mongo Atlas Client to see if the user's details were registered.

With this , we've finished the process of registering our user.

Let's move on to building out the /login route next.

Building our Login route 🌈

Building the login system involves the same validation process as registering our users. Go ahead and paste the following code in your file inside the try block of your login route.

We're also going to use MongoDB's findOne() method to extract the credentials of the corresponding email that the user had entered. We will store this inside a user variable.

    //Use Login Values Validator
            const { error } = loginValidator(req.body);
            if (error) return res.status(400).send(error.details[0].message)

            //UserExistCheck
            const user = await User.findOne({ email: req.body.email });
            if (!user) return res.status(400).send('Account does not exist with provided email and password combination.');

Comparing the hashed password with the entered password

To compare our passwords, we're going to make use of bcrypt's .compare() method. This method takes the user's entered password as its first parameter and the hashed password stored in the DB that we extracted earlier.

const validPassword = await bcrypt.compare(req.body.password, user.password);
            if (!validPassword) return res.status(400).send('Incorrect Password');

The code above stores the bool result of the bcrypt.compare() method. If the password is invalid , we return a message "Incorrect Password" with a status code of 400.

Finally , we'll return a success message back to the user to simulate a successful login attempt using the res.send() method.

res.send("Login Successful!")

Finally . you can test this out in your rest.http file by making a POST request to /api/auth/login with valid credentials. If all goes well , you should now see the "Login Successful" message!

Congratulations! 🎉

You've just built a login/registration system using Express and MongoDB.

In the next part , we're going to deal with JWTifying our Authentication/Authorisation process. 👨🏻‍💻

Discussion (0)