How to Build Simple and Secure REST API for User Authentication Using Node.js, JWT, and MongoDB

Pranesh A S
The Startup
Published in
10 min readJan 14, 2021

--

Image by Pete Linforth from Pixabay

Welcome! In this article, we will be developing a secure and lightweight REST API using Node.js, Express server, and MongoDB from scratch that can be used as a backend for authentication systems. This is completely a beginner-friendly article.

As a bonus, I have explained how to create a simple referral system, using which you can share the referral code, and your friends can signup using that code. The concepts we will see throughout this article are completely generic, and it can be implemented using any programming language.

Agenda ✍️:

  1. User signup/registration with Email verification.
  2. User Login.
  3. Forgot password and reset password.
  4. Session management using JWT (JSON Web Tokens).
  5. JWT gotchas
  6. Bonus: Simple Referral System!

In this part only the first 3 points will be covered. The remaining features are implemented in Part II.

Preparation 🏃:

Project Setup 📑

Initialize a fresh Node.js project by running the npm init command in the application root folder and answer the questions. If you want to set default values to all the questions, you can add the -- y flag, like npm init --y. Here, we are trying to create a node application with a basic configuration.

If you check your project folder, you’ll see a tiny file called the “package.json” created by the npm init command.

Note: We will not install all the dependencies at once. We will install them only at that particular step.

Let us spin up the express server. For doing that, install the express module using the npm i express --savecommand.

After installation, create a file “app.js” in the application root directory.

Now save the file and run node app.js command in your terminal or command prompt. You must see the following output.

Server started listening on PORT : 5000

Now let’s test it by hitting the “/ping” endpoint from the Postman.

Yay! Isn’t it cool? We have created a local web server that can handle HTTP requests using Express.

Let us connect to MongoDB from our application. To do so, we need to install a couple of dependencies by running :

npm i mongoose dotenv body-parser --save
  • Mongoose : An Object Data Modeling (ODM) library for MongoDB and Node.js.
  • Dotenv : Used to load environment variables.
  • Body-parser : Helps to parse the incoming request bodies so that we can access using the req.body convention. If you are new to this don’t worry, you’ll catch up in a moment.

You can either use MongoDB Atlas or Local mongo server. There are many articles to help you get the connection string from Atlas.

For Manual installation : https://docs.mongodb.com/manual/installation/

Once you are ready with it, create a file called .env in the project root folder.

Add this line to the .env file:

MONGO_URI = <MONGO_CONNECTION_STRING>

If you are using MongoDB Atlas, your connection string starts like mongodb+srv://…

I’m using local server for this server.

mongodb://127.0.0.1:27017/TheNodeAuth is my connection string.

We can connect our application to the mongodb server by importing the mongoose package.

Once the changes are done, lets run our project :node app.js

If everything is fine, the your output will be something similar to this 👇:

Server started listening on PORT : 5000
Database connection Success.

That’s great. We have successfully completed our project setup. Now let us start building the REST APIs for user authentication.

Creating the User Schema :

Create a folder called src inside the project root directory. This is the folder inside which we will create all the required files, that will handle user schema modeling, business logic, helper functions, etc.

Inside the src folder, create another folder called users.

Q :Why we need to structure our project this way?

Ans: This is one of the best practices to split the project based on its features.

Okay, inside the “users” folder, create a file called user.model.js. I personally prefer this kind of naming convention. You can also use the method you prefer.

We have created the user schema and exported it. Now we need to import it and start defining the logic for authentication.

Now create a file called user.controller.js inside the src/users folder. Inside this file, we will implement all the logic for all the features like user signup, login, reset password, etc.

User Signup :

For signup, we need to validate the request body first. Then we need to generate a unique id for each user. One can argue that the mongoose itself will generate a unique id for each document. AFAIK, it is safe to use and expose the ObjectId only up to some extent. Hence we will use a well-known unique id generator, “UUID.”

Also we need to validate the incoming request, ie., we need to check whether the email has been entered, whether it is a valid email id, all the required fields are present, the minimum length of the password, etc. Though these things can be done at the client-side, it is not bad to add an extra layer of security to our application by adding a server-side validation.

For this purpose, we will use the joi package.

Note ✍️: Feel free to explore all the npm packages that I mention. I’ll not be giving too much detail about every package that we use, to keep this article crisp and clear.

So, lets install the packages needed: npm i joi uuid --save

Flow :

  1. Validate the user entered fields (email, password, confirm password) using joi.
  2. Check whether already an account with the given email exists in our database.
  3. If it exists, then throw an error.
  4. If not, then hash the password using bcryptjs npm module.
  5. Generate a unique user id using the uuid module.
  6. Generate a random 6 digit token ( with an expiry time of 15 minutes ) and send a verification email to the user’s email id.
  7. Then save the user in the database and send a “success” response to the client.

To send email to the users, we will use the nodemailer module along with the Sendgrid SMTP credentials. Signup at SendGrid (yes, it’s free) and place your SendGrid API key in the .env file.

SG_APIKEY = <YOUR SENDGRID KEY>

Install the nodemailer npm package: npm i nodemailer --save

We need some helper functions and additional modules to hash the password, send verification code to email, and so on.

Inside the src/users directory, create a new folder called helpers create a file mailer.js inside the helpers folder.

That’s it, save the file.

Now let us create a function to hash the password. We will use brycptjs to hash the password. To install the module, run: npm i bcryptjs --save.

In the user.model.js file, import the module as

const bcrypt = require(‘bryptjs’);

At the bottom of the file, define the function to hash the password.

module.exports.hashPassword = async (password) => {
try {
const salt = await bcrypt.genSalt(10); // 10 rounds
return await bcrypt.hash(password, salt);
} catch (error) {
throw new Error("Hashing failed", error);
}
};

Below the resetPasswordExpires field, add these two fields and save the file.

emailToken: { type: String, default: null },
emailTokenExpires: { type: Date, default: null },

Lets implement the Signup feature.

Let us define the endpoint for signup. Create a folder routes in the project root directory and inside the folder create a file called “users.js”.

routes/users.js :

const express = require("express");
const router = express.Router();
const cleanBody = require("../middlewares/cleanbody");
const AuthController = require("../src/users/user.controller");
router.post("/signup", cleanBody, AuthController.Signup);module.exports = router;

You should have noticed that I have used something called cleanbody.

As we deal with database operations with the data in the request body, it is recommended to sanitize the request body before starting to process them to avoid security issues. You can learn more about this here.

We will use the npm package called mongo-sanitize to sanitize the request body.

npm i mongo-sanitize --save.

Create a folder called middlewares in the project root directory and create a file “cleanbody.js”.

Save the file. Now require the routes/users.js file in the app.js file.To do so right above the app.listen(…) line add the following line:

app.use(“/users”, require(“./routes/users”));

Save the file and fire the server node app.js. Once the server has been started, and the database connection is established, switch to the postman.

Lets test the signup flow:

It works! The above error is thrown from the “joi” validator, which we have defined in our controller file. Now let’s try again with a valid request.

Ok success! Lets verify, by checking the database and the email inbox.

Congratulations ! We have completed the User registration. 💥💥

User Login :

After successful signup, we must allow the user to login.Lets create the login function in the user.controller.js.

Below the signup function in user.controller.js file, add the following code.

Define the login endpoint in the routes/users.js file by adding these lines after the signup endpoint.

router.post(“/login”, cleanBody, AuthController.Login)

Save the files and restart the server.

💡 Tip : You can install development tools like nodemon if it’s frustrating to restart the server every single time. Configuring tools, like nodemon, will keep watching the code base for changes. As soon as you save any of the project’s files, it’ll automatically restart the server for you.

Lets try to test login, using the Postman.

Oops! We get an error message from which we can infer that we cannot log in without activating the account via email verification. Let’s start building the logic for account activation. We will just get the email and the code (OTP sent to email during registration) from the user and verify whether it is valid.

User.controller.js :

Right below the login controller, add the following code :

Don’t forget to define the endpoint for account activation in the routes/users.js file.

router.patch(“/activate”, cleanBody, AuthController.Activate)

Save the file and restart the file. Lets try to activate our account.

Cool! Let’s move on to implement the forgot password and reset password feature as they are pretty straight forward.

Forgot Password: Get the email from the user, check if the email is present in our DB. If present, we will generate a code valid for 15 mins (resetPasswordToken and resetPasswordTokenExpires) and send it to the email.

Reset Password: Get the code, new password, and confirm password from the user. If the code is valid and passwords match each other, then hash the new password and save it in DB. If not, then throw an error.

In the user.controller.js below activate controller, add these two functions.

Define the endpoints for the forgot password and reset password features in route/user.js file

router.patch(“/forgot”, cleanBody, AuthController.ForgotPassword);
router.patch(“/reset”, cleanBody, AuthController.ResetPassword);

Save the files, fire the server and switch to Postman.

Lets check in our DB .

Note: The standard method to implement a reset password feature is to generate a long random string (length at least >16) and send a reset link to the email. But in this article, I’m keeping it simple.

It works. The code will be delivered to the email. Lets go ahead and change our password

Now lets check whether the changes are reflected in DB.

Yes! You can see that the resetPasswordToken and resetPasswordExpires have changed to “null,” the hash in the password field has also been updated.

Now lets login with our old password

Okay, let us try with our new password.

Cool 💪 Everything is working well. Till now, we have implemented User signup, login, account activation, forgot password, and reset password along with some helper functions to sanitize the body, sending emails, and so on.

You can access full source code here.

Conclusion :

Next, we need to implement user session management via JSON Web Tokens and a Simple referral system. I think this article is already too big. Hence I decided to break this into two parts. So we will continue implementing the remaining features in Part II. Meanwhile, feel free to share your thoughts in the comments section, I’ll be happy to discuss.

Thank you for your time!

--

--

Pranesh A S
The Startup

Backend Engineer and Blockchain Developer. Keep learning | Spread Knowledge