Create secure login, signup using Express and JWT.

Almost all web applications in the world have a common functionality. That is login and signup functionality for user authentication. Thus it becomes important to make this functionality as secure as possible. Most modern websites authenticate users using authentication tokens. These tokens are sent in API request header along with other data to authenticate a user. In this post, we will learn how to create this setup. We will be using Express, JSON Web Tokens (JWT) and Mongoose for the same.

First of all, create a new empty folder. Name it whatever you want to. Then cd to it. Then run the following command: npm init . This will create few basic files required in a Node.JS app.

Then we will have to install all the required libraries. We will be installing the following libraries:

  1. Express: It is a modular web framework for Node.js which is used for easier creation of web applications and services.
  2. Mongoose: It is an Object Data Modeling (ODM) library for MongoDB and Node.js . It is basically used to acess MongoDB from web applications for storing data.
  3. Express-validator: It is used to validate incoming HTTP requests. It checks if a request has all the required headers, data in correct format etc. For example, if we are sending an Email ID in API request, it can check if format of Email address is correct or not (i.e abc@def.com)
  4. Bcrypt: It is used to encrypt data like passwords before storing them in MongoDB.
  5. JWT: JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. We will use it to generate auth tokens.
  6. Config: It is used to store default parameters like API keys, URIs and call them for use from other files.

To install the libraries, cd to project folder, and run the following command:

npm i express mongoose express-validator bcrypt jsonwebtoken config --save

Setting up MongoDB and Mongoose.

We will use MongoDB cloud for storing user data. For this, go to https://cloud.mongodb.com and create a free account. Then create a new project. Name it as you wish. Now inside this project, create a cluster. Select ‘Starter Cluster’ which is free. Select a cloud provider (preferably AWS) and region according to your location. After cluster is created, go to Network Access and whitelist your IP address there. After that, click on ‘connect’. Create a MongoDB user with any preferred username and password (you will require them later). After the user is created, click on ‘Choose a connection method’. Inside it select ‘Connect to your application’ and copy the connection string somewhere.

Now inside your project folder, create a subfolder named models. Inside this folder, create a file User.js . This file will define the document schema which will store user data. Inside User.js, add the following code:

// models/User.js

const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    email: {
        type: String,
        required: true,
        unique: true
    },
    password: {
        type: String,
        required: true
    }
});

module.exports = User = mongoose.model("user", UserSchema);

As we can see, the User document contains the name, email and password of the user. Also apart from these fields, MongoDB creates another field called id by itself inside every document. We will use this id to identify users in some time.

Now inside the project root directory, create another folder named config. Inside this folder, create a file default.json . This file will store our MongoDB connection string which we just copied from website. We will pull this string from this file via the ‘config’ library we just installed. Here is a sample default.json for you.

{
  "mongoURI": "mongodb+srv://<username>:<password>@cluster0-y050h.mongodb.net/test?retryWrites=true&w=majority"
}

Use your own connection string instead of copying above and replace the <username> and <password> with your own credentials which you defined while creating your cluster.

Now again inside the config folder, create a file db.js. Inside this file, add the following code:

// config/db.js

const mongoose = require('mongoose');
const config = require('config');

const db = config.get('mongoURI');

const connectDB = async () => {
  try {
    await mongoose.connect(db, { useNewUrlParser: true, useCreateIndex: true });
    console.log('MongoDB connected...');
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }
};

module.exports = connectDB;

As you can see, in the above code, we fetched the ‘mongoURI‘ we defined in default.json using config.get('mongoURI');. Then we use this string to connect to the MongoDB cloud using the mongoose.connect() function.

Setting up JWT.

Now since we have database set up, we will work on setting up user authentication. The way this will work is: the user will send an auth token in the header while making an API request. This token will be used to verify identity of the user and make changes accordingly. If token does not correspond to any user, we will declare the request as invalid.

Let us understand first how JWT tokens work. First we have to define a private key which only admin of the web app knows. This private key, along with the auth token help us identify if the token is valid or not. We will define our private key inside config/default.json. It can be any secret string. After adding this secret key, default.json will look like this:

{
  "mongoURI": "mongodb+srv://<username>:<password>@cluster0-y050h.mongodb.net/test?retryWrites=true&w=majority",
  "jwtSecret": "private_key_goes_here"
}

Now inside root folder, create a folder named middleware. Inside it, create a file auth.js. Inside this file, add the following code:

// middleware/auth.js

const jwt = require("jsonwebtoken");
const config = require("config");

module.exports = function(req, res, next) {
    const token = req.header("x-auth-token");

    if (!token) {
        return res.status(401).json({ msg: "No token, authorization denied!" });
    }

    // verify token:
    try {
        const decoded = jwt.verify(token, config.get("jwtSecret"));
        req.user = decoded.user;
        next();
    } catch (err) {
        res.status(401).json({ msg: "token not valid!" });
    }
};

In the above code, we first fetch token from the request header. Then we pass this token to jwt.verify() function along with the secret key we defined in default.json. This function will return an object which we then used to get the user. Note that decoded.user doesn’t return the actual username, it returns the id which MongoDB assigns to every document stored in it. Now we will simply call this exported function from other files to authenticate a user.

Creating Express routes

Create a folder routes inside the root directory. Inside it, create a file userauth.js. We will define signup, login and userinfo routes in this file. First of all let us work on signup route. It will ask for name, email and password from user. If no such user already exists, then we will add these credentials in the MongoDB database.

// routes/userauth.js

const express = require("express");
const router = express.Router();
const { check, validationResult } = require("express-validator");
const User = require("../models/User");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const config = require("config");
const auth = require("../middleware/auth");

router.post(
  "/signup",
  [
    check("name", "Name is required.")
      .not()
      .isEmpty(),
    check("email", "Please use valid email.").isEmail(),
    check("password", "password >= 6 char").isLength({ min: 6 })
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    const { name, email, password } = req.body;
    try {
      let user = await User.findOne({ email });
      if (user) {
        return res
          .status(400)
          .json({ errors: [{ msg: "User already exists!" }] });
      }

      user = new User({ name, email, password });
      const salt = await bcrypt.genSalt(10);
      user.password = await bcrypt.hash(password, salt);
      await user.save();

      const payload = {
        user: {
          id: user.id
        }
      };
      jwt.sign(
        payload,
        config.get("jwtSecret"),
        { expiresIn: 36000 },
        (err, token) => {
          if (err) throw err;
          res.json({ token });
        }
      );
    } catch (err) {
      console.error(err.message);
      res.status(500).send("Server error.");
    }
  }
);

In the above code, we first check whether the username, password and email are in correct format using the check() and validationResult() function of express-validator. After that, we fetch the username, email, password from request body. Then we check that user does not already exist using await User.findOne() method. findOne() method tries to find any other existing person with same email inside User model. If no other person with same email exists, then it is safe to create this user. Then we create a new User object using these credentials. We then encrypt the password field of this User object using bcrypt.genSalt() and bcrypt.hash() methods. Finally we save this user in MongoDB document.

In order to create an auth token of this new user, we use jwt.sign() function. We pass our jwt secret key and user ID as payload to this function. Then we send this created token as a request response using res.json({ token });

Next we will create login route. For this, inside userauth.js, add the following code below signup route.

// routes/userauth.js
// after signup route...

router.post(
  "/login",
  [
    check("email", "Please use valid email.").isEmail(),
    check("password", "password >= 6 char").isLength({ min: 6 })
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json(errors["errors"]);
    }

    const { email, password } = req.body;
    try {
      const user = await User.findOne({ email });
      if (!user) {
        return res
          .status(400)
          .json({ errors: [{ msg: "User does not exists!" }] });
      }
      const isMatch = await bcrypt.compare(password, user.password);
      if (!isMatch) {
        return res
          .status(400)
          .json({ errors: [{ msg: "invalid credentials." }] });
      }
      const payload = {
        user: {
          id: user.id
        }
      };
      jwt.sign(
        payload,
        config.get("jwtSecret"),
        { expiresIn: 36000 },
        (err, token) => {
          if (err) throw err;
          res.json({ token });
          console.log("auth token from login: ", token);
        }
      );
    } catch (err) {
      console.error(err.message);
      res.status(500).send("Server error.");
    }
  }
);

In the above code, we again check the validity of email, password using express-validator. Then we try to find if a user with the given email ID exists in User document or not. If no, then we return “user does not exists!”. If given email exists in database, then we try to compare the given password with the encrypted password stored in User document. For this, we use bcrypt.compare(). If passwords match, then the login is genuine. Then we again use jwt.sign() to generate our auth token as done before. We then return this token as request response.

Now since we have created login and signup functionality, we will create another route userinfo to test the auth token generated from above routes. This route will display name and email of the user corresponding to the auth token. Notice that this route will be private. That means only person with valid auth token will be able to access it (user will get auth token only after successful login). Inside userauth.js, add the following code:

//routes/userauth.js

// after signup, login routes...

router.get("/userinfo", auth, async (req, res) => {
  try {
    const user = await User.findById(req.user.id).select("-password");
    res.json({ user });
  } catch (err2) {
    console.error(err.message);
    res.status(500).send("Server down.");
  }
});
// export the router
module.exports = router;

In the above code, notice that we called auth as an argument in router.get(). This is the same function which we defined in middleware/auth.js. It will take care of checking the validity of the auth token. This way, we will be saved from writing same code for checking token in every private route. Then we use User.findById() to find the user corresponding to the input id. Also select("-password") removes password from the user data as we will not be displaying it.

Configuring the server.

Create a file server.js inside the root directory. Add the following code to it:

const express = require("express");
const app = express();
const connectDB = require("./config/db");

connectDB();

app.use(express.json({ extended: false }));

app.get("/", (req, res) => res.send("API running"));

app.use("/userauth", require("./routes/userauth"));

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

This file calls the function defined in config/db.js to connect to MongoDB. Also here we define all the API route URLs and their corresponding route files.

Also install Nodemon using command npm i nodemon --save. Nodemon enables faster restart of server whenever any code changes are made, thus saving us from manually restarting server which consumes time.

Make changes in package.json accordingly:

{
  "name": "loginsignup",
  "version": "1.0.0",
  "description": "",
  "main": "server.js",
  "scripts": {
    "start": "node server",
    "server": "nodemon server"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcrypt": "^3.0.6",
    "config": "^3.2.3",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-validator": "^6.2.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.7.4",
    "nodemon": "^1.19.3"
  }
}

Running and Testing the code.

Now comes the time to test our project. Run the server using command: npm run server. Now open Postman and send API requests as shown below:

POST request:- http://localhost:5000/userauth/signup
Headers:- Content-Type: application/json
Body:- 
{
	"name": "tarunk",
	"email": "abcde@gmail.com",
	"password": "123456"
}

Send this request and you should get auth token in the following format:

{
    "token": "eyJhbGciOiJIUzI1NiI... ... ..."
}

Now since this user is created, we can use these credentials to login. Send another request with the following formant:

POST request:- http://localhost:5000/userauth/login
Headers:- Content-Type: application/json
Body:- 
{
	"email": "abcde@gmail.com",
	"password": "123456"
}

Send this request and you should get the same auth token as response:

{
    "token": "eyJhbGciOiJIUzI1NiI... ... ..."
}

Copy this token. Now to display the user info of this user, we will use this token. Send another request in the following format:

GET request:- http://localhost:5000/userauth/userinfo
Headers:- x-auth-token: eyJhbGciOiJIUzI1NiI... ... ... // your auth token

This should display the user info in the following format:

{
    "user": {
        "_id": "5dd03871e8e2b504a4501836",
        "name": "tarunk",
        "email": "abcde@gmail.com",
        "__v": 0
    }
}

That’s it! We have successfully create signup and login functionality. Hope you liked the article. Comment in case of any doubts. Also check out my articles on medium and follow me on twitter. Also check out my other articles on this blog as well!

Leave a Reply

Your email address will not be published. Required fields are marked *