Build Secure (JWT) Token Based Auth API with Node

Last Updated on by in Node JS

In this tutorial, we are going to learn how to build a secure token-based user authentication REST APIs using JWT (JSON web token), bcrypt, Node, Express, and MongoDB.

Creating authentication REST API with Node Js is merely effortless.

We will be taking the help of Express js to create the authentication endpoints and also make the MongoDB connection to store the user’s data in it.

What are JSON Web Tokens (JWT)?

JSON Web Token (JWT) is a JSON object that is described in RFC 7519 as a safe approach to transfer a set of information between two parties.

The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure.

Enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.
– Internet Engineering Task Force (IETF)

Authentication Workflow with JSON Web Tokens

Let’s understand from the below diagram how does the secure authentication system work with JSON web token.

Token Based Authentication API
image source medium.com

  • A client makes the API call and sends the user information such as username and password to the webserver.
  • On successful authentication a webserver generates a string-based token and returns to the client system.
  • A client can store this token in the browser’s local storage or in a session.
  • Client sets this token in a header something like “Bearer xxx.xxx.xxx”.
  • On next API call JWT token communicateS with the server, and after the successful verification, the server returns the response to the client.

Node and Express JWT Token Based Auth REST API Example

  • Step 1: Create Node Project
  • Step 2: Create Mongoose Schema
  • Step 3: Define Mongoose Schema
  • Step 4: Verify Node Authentication REST API
  • Step 5: Adding Input Validation in Express RESTful API
  • Step 6: Secure Token-based Auth REST API
  • Step 7: Node Server Configuration
  • Step 8: Start Node Server

Initiate Node Token-Based Authentication Project

Create a project folder to build secure user authentication REST API, run the following command.

mkdir server

Get inside the project folder.

cd server

Let’s start the project by first creating the package.json file by running the following command.

npm init -y

Install NPM Packages to Create Secure Auth API

Next, install the NPM dependencies for the authentication API by running the given below command.

npm install bcrypt body-parser config cors dotenv express express-validator jsonwebtoken mongoose mongoose-unique-validator
Package Detail
jsonwebtoken This package is used to create the expirable and robust string-based token, that helps in making the secure communication between client and the server.
bcryptjs This package hash the password into the protected string before saving it to the database.
mongoose-unique-validator The mongoose-unique-validator is a package that attaches pre-save validation for unique fields within a Mongoose schema.
body-parser The body-parser npm module is a JSON parsing middleware. It helps to parse the JSON data, plain text or a whole object.
Express Express js is a free open source Node js web application framework. It helps in creating web applications and RESTful APIs.
CORS This is a Node JS package, also known as the express js middleware. It allows enabling CORS with multiple options. It is available through the npm registry.
Mongoose Mongoose is a MongoDB ODM for Node. It allows you to interact with MongoDB database.

Next, install the nodemon NPM module, it helps in starting the node server when any change occurs in the server files.

npm install nodemon --save-dev

--save-dev saves the package for development purpose.

Define Mongoose Schema

Next, we are going to define user schema using mongoose ODM. It allows us to retrieve the data from the database.

Have a look on the below tutorial to setup mongoDB on macOS and window.

Create a folder and name it models inside the project directory, create a file User.js in it.

Earlier we installed mongoose-unique-validator, this package used to validate duplicate email id from MongoDB database.

Next, add the following code in models/User.js file:

// models/User.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const uniqueValidator = require('mongoose-unique-validator');

let userSchema = new Schema({
    name: {
        type: String
    },
    email: {
        type: String,
        unique: true
    },
    password: {
        type: String
    }
}, {
    collection: 'users'
})

userSchema.plugin(uniqueValidator, { message: 'Email already in use.' });
module.exports = mongoose.model('User', userSchema)

The userSchema.plugin(uniqueValidator) method won’t let duplicate email id to be stored in the database.

The unique: true property in email schema does the internal optimization to enhance the performance.

Verify Node Authentication REST API

Next, we will verify the auth API using the JWT token. Create a middlewares folder and create a auth.js file.

Add the below code in the ‘middlewares/auth.js’ file.

const jwt = require("jsonwebtoken");

module.exports = (req, res, next) => {
  try {
    const token = req.headers.authorization.split(" ")[1];
    jwt.verify(token, "longer-secret-is-better");
    next();
  } catch (error) {
    res.status(401).json({ message: "No token provided" });
  }
};

Note: In the real world app the secret should not be kept in the code as declared below. The best practice is to store as an environment variable and it should be complex combination of numbers and strings.

We added the authorize variable inside the user-profile API. It won’t render the data unless it has the valid JWT token. As you can see in the below screenshot, we have not defined the JWT token in get request, so we are getting the “No token provided” error.

JWT Verification Error in Express API

Adding Input Validation in Express RESTful API

To validate the express auth post request we are using bcrypt js 7+ version along with express router. Also, we will throw light on hashing the password using salt and hash properties in node and express.

The express-validator is an express.js middleware for validating POST body requests.

Here is how you can add validation in middlewares/auth.routes.js file.

const { check, validationResult } = require("express-validator");

// Sign-up
router.post(
  "/register-user",
  [
    check("name")
      .not()
      .isEmpty()
      .isLength({ min: 3 })
      .withMessage("Name must be atleast 3 characters long"),
    check("email", "Email is required").not().isEmpty(),
    check("password", "Password should be between 5 to 8 characters long")
      .not()
      .isEmpty()
      .isLength({ min: 5, max: 8 }),
  ],
  (req, res, next) => {
    const errors = validationResult(req);
    const saltRounds = 10;
    if (!errors.isEmpty()) {
      return res.status(422).jsonp(errors.array());
    } else {
      bcrypt.genSalt(saltRounds, function (err, salt) {
        bcrypt.hash(req.body.password, salt, (err, hash) => {
          const user = new userSchema({
            name: req.body.name,
            email: req.body.email,
            password: hash,
          });
          user
            .save()
            .then((response) => {
              res.status(201).json({
                message: "User successfully created!",
                result: response,
              });
            })
            .catch((error) => {
              res.status(500).json({
                error: error,
              });
            });
        });
      });
    }
  }
);

We Passed the validation array with the check() method inside the post() method as a second argument.

Next, we called the validationResult() method to validate errors, and it returns the errors if found any.

Following validation we implemented in ("/register-user") api.

  • Check if the value is required.
  • Check min and max character’s length.

Express input validation

Create Secure Token-based Authentication REST API in Node

To build secure user authentication endpoints in node, create routes folder, and auth.routes.js file in it.

Here, we will define CRUD Restful APIs using the npm packages for log-in, sign-up, update-user, and delete-user.

const express = require("express");
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
const router = express.Router();
const userSchema = require("../models/User");
const authorize = require("../middlewares/auth");
const { validationResult } = require("express-validator");
const cors = require("cors");

// CORS OPTIONS
var whitelist = ["http://localhost:4200", "http://localhost:4000"];
var corsOptionsDelegate = function (req, callback) {
  var corsOptions;
  if (whitelist.indexOf(req.header("Origin")) !== -1) {
    corsOptions = {
      origin: "*",
      methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
    };
  } else {
    corsOptions = { origin: false }; // disable CORS for this request
  }
  callback(null, corsOptions);
};

// Sign-up
router.post("/register-user", (req, res, next) => {
  const err = validationResult(req);
  if (err.isEmpty()) {
    bcrypt.hash(req.body.password, 20).then((hash) => {
      const user = new userSchema({
        name: req.body.name,
        email: req.body.email,
        password: hash,
      });
      user
        .save()
        .then((result) => {
          res.status(201).json({
            message: "User successfully created!",
            result: result,
          });
        })
        .catch((err) => {
          res.status(500).json({
            error: err,
          });
        });
    });
  } else {
    return res.status(422).jsonp(errors.array());
  }
});

// Sign-in
router.post("/signin", (req, res, next) => {
  let getUser;
  userSchema
    .findOne({
      email: req.body.email,
    })
    .then((user) => {
      console.log(user)
      if (!user) {
        return res.status(401).json({
          message: "Authentication failed",
        });
      }
      getUser = user;
      return bcrypt.compare(req.body.password, user.password);
    })
    .then((response) => {
      if (!response) {
        return res.status(401).json({
          message: "Authentication failed",
        });
      }
      let jwtToken = jwt.sign(
        {
          email: getUser.email,
          userId: getUser._id,
        },
        "longer-secret-is-better",
        {
          expiresIn: "1h",
        }
      );
      res.status(200).json({
        token: jwtToken,
        expiresIn: 3600,
        _id: getUser._id,
      });
    })
    .catch((err) => {
      return res.status(401).json({
        message: "Authentication failed" + err,
      });
    });
});

// Get Users
router.route("/", cors(corsOptionsDelegate)).get(async (req, res, next) => {
  await userSchema
    .find()
    .then((result) => {
      res.writeHead(201, { "Content-Type": "application/json" });
      res.end(JSON.stringify(result));
    })
    .catch((err) => {
      return next(err);
    });
});

// Get Single User
router.route("/user-profile/:id").get(authorize, async (req, res, next) => {
  await userSchema
    .findById(req.params.id, req.body)
    .then((result) => {
      res.json({
        data: result,
        message: "Data successfully retrieved.",
        status: 200,
      });
    })
    .catch((err) => {
      return next(err);
    });
});

// Update User
router.route("/update-user/:id").put(async (req, res, next) => {
  await userSchema
    .findByIdAndUpdate(req.params.id, {
      $set: req.body,
    })
    .then((result) => {
      res.json({
        data: result,
        msg: "Data successfully updated.",
      });
    })
    .catch((err) => {
      console.log(err);
    });
});

// Delete User
router.route("/delete-user/:id").delete(async (req, res, next) => {
  await userSchema
    .findByIdAndRemove(req.params.id)
    .then(() => {
      res.json({
        msg: "Data successfully updated.",
      });
    })
    .catch((err) => {
      console.log(err);
    });
});

module.exports = router;

To secure the password, we are using the bcryptjs, It stores the hashed password in the database.

In the signin API, we are checking whether the assigned and retrieved passwords are the same or not using the bcrypt.compare() method.

In the signin API, we set the JWT token expiration time. Token will be expired within the defined duration.

Node Server Configuration

Create a index.js file in the token-based authentication project’s folder and paste the following code in it.

const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");

// Express APIs
const api = require("./routes/auth.routes");

// Connecting MongoDB
async function mongoDbConnection() {
  await mongoose.connect(
    "mongodb://127.0.0.1:27017/test",
    {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    },
    6000
  );
}
mongoDbConnection().then(() => {
  console.log("MongoDB successfully connected.");
}),(err) => {
    console.log("Could not connected to database : " + err);
  };

// Express settings
const app = express();
app.use(bodyParser.json());
app.use(
  bodyParser.urlencoded({
    extended: false,
  })
);
app.use(cors());

// Serve static resources
app.use("/public", express.static("public"));
app.use("/api", api);

// Define PORT
const port = process.env.PORT || 4000;

const server = app.listen(port, () => {
  console.log("Connected to port " + port);
});

// Express error handling
app.use((req, res, next) => {
  setImmediate(() => {
    next(new Error("Something went wrong"));
  });
});

app.use(function (err, req, res, next) {
  console.error(err.message);
  if (!err.statusCode) err.statusCode = 500;
  res.status(err.statusCode).send(err.message);
});

In this file we defined mongoDB database, express routes, PORT and errors.

Start Node Server

Now, we have placed everything at its place, and now it’s time to start the Node and MongoDB server.

Make sure to setup and follow the guide to start the MongoDB community and MongoDB Compass GUI database in your local system.

Also, pass the MongoDB database URL to the mongoose.connect(‘…’) method in the backend/index.js file.

Start the nodemon server:

npx nodemon server

You can test Node server on the following URL:
http://localhost:4000/api

Here, are the user authentication CRUD REST APIs built with Node.js.

API Methods API URL
GET (Users List) /api
POST (Sign in) /api/signin
POST (Sign up) /api/register-user
GET (User Profile) /api/user-profile/id
PUT (Update User) /api/update-user/id
DELETE (Delete User) /api/delete-user/id

Click on the below button to get the complete code of this project on GitHub.

Git Repo

Conclusion

Finally, we have completed secure Token-Based Authentication REST API with Node.js tutorial.

So far, In this tutorial we have learned how to securely store the password in the database using the hash method with bcryptjs. How to create JWT token to communicate with the client and a server using jsonwebtoken.

We also implemented the Express input validation using the express-validator plugin.

I hope you liked this tutorial, please share it with others, thanks for reading!