User Authentication in MERN Stack — Part 1 (Backend)

Anand Shrestha
The Startup
Published in
10 min readAug 30, 2020

In this tutorial, we will be creating a simple application for user authentication using MERN Stack(MongoDB for our database, Express and Node for our backend, and React for our frontend). 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. We will use react and react-hook for state management on our frontend.

This article will focus only on the usage of JWT for providing authentication to our REST APIs. If you want to read more about Authentication Workflow with JSON Web Tokens, I suggest you follow this article.

In this part (Serverside), we will cover the following topics:

  • Set up backend using npm and install the necessary packages.
  • Set up a MongoDB database using mongoDB atlas cloud.
  • Create a database schema to define a User for registration and login purposes.
  • Set up two API routes register and login using jsonwebtokenfor authentication and build input validation without any dependencies.
  • Test our API routes using Postman.

Pre-requisites

Before we get started, install all the tools we are going to need to set up our application.

Set up backend

Create a project folder in your workspace to build REST API (backend) and run the following command.

npm init

After running the command, package.json will be created and set up index.js as the default entry point.

Install NPM Packages

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

npm i express jsonwebtoken bcrypt body-parser 
cors mongoose dotenv

Brief information about each package and why we are using this to build rest APIs

  • express: Express is a nodejs web application framework that helps in creating rest APIs.
  • brcypt: A library to hash passwords.
  • body-parser: It is used to parse incoming request bodies in a middleware.
  • jsonwebtoken: This package creates a token used for authorization for secure communication between client and server.
  • mongoose: Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node. js that allows you to interact with MongoDB database.
  • cors: CORS is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
  • dotenv: Dotenv is a zero-dependency module that loads environment variables from a .env file.

Let's install nodemon which helps in monitoring and starting the node server when any change occurs in the server files.

npm i -D nodemon

Our package.json should look like following at this moment.

{
"name": "mern-auth",
"version": "1.0.0",
"description": "MERN Stack User Authentication",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Anand Shrestha",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.9.27"
},
"devDependencies": {
"nodemon": "^2.0.4"
}
}

Setting up our express.js server

Here’s the simplest form of code: index.js that says Welcome to the mern auth tutorial! Server is running on 8000on the console when you run npm start

const express = require('express');
const app = express();
const port = 8000;app.listen(port, () => {
console.log(`Welcome to the mern auth tutorial! Server is running on ${port}`)
});

Setting up the env file and connecting our server with MongoDB

Let’s create .env file on the root folder by running the commandtouch .env

Before we created our schema, have a look at this tutorial to set up a MongoDB database on MongoDB atlas cloud. This article provides detailed information on how to set up MongoDB atlas.

By following the above article you will have MongoDB URL which we will connect with our express server.

Add the following variables for port and database URL on our .env file.

PORT=8000
DATABASE=mongodb+srv://<username>:<password>@cluster0.suxf5.mongodb.net/<dbname>?retryWrites=true&w=majority

Replace your username(<username> and password(<password>) of MongoDB atlas cloud along with the database name you created(<dbname>).

Next, we are going to test our MongoDB connection with our express server. Add the following code on our index.js file

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
require('dotenv').config();
//app
const app = express();
// db
mongoose
.connect(process.env.DATABASE,{
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
})
.then(() => console.log('DB Connected'));

//middlewares
app.use(bodyParser.json());
app.use(cors());
const port = process.env.PORT || 8000;app.listen(port, () => {
console.log(`Server is running on ${port}`)
});

You will see the following on your console.

Congratulations!! we have successfully connected our MongoDB database with our express server.

Define User Schema

Next, we are going to define user schema using mongoose ODM. It allows us to retrieve the data from the database. Let’s create a models folder to define our user schema and create a User.js file in it.

mkdir models
cd models
touch User.js

Add the following code in models/User.js file:

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
let userSchema = new Schema({
name:{
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
}
},{
timestamps: true,
collection: 'users'
})
module.exports = mongoose.model('User', userSchema);

This is pretty straightforward, we have created user schema by defining fields and types as objects of the Schema.

Set up Secure Token-based Authentication REST APIs in Node

To build secure user authentication Rest APIs in node, let’s create routes folder and auth.js file in it.

mkdir routes
cd routes
touch auth.js

Here, we will define two endpoints signup and signin like following.

const express = require('express');
const router = express.Router();
const { signup, signin } = require('../controllers/auth');router.post('/signup', signup);
router.post('/signin', signin);
module.exports = router;

We have imported our two methods signin and signup from controllers which we haven’t created yet so let’s create controllers folder and add auth.js file in it and put all our authentication logic there.

mkdir controllers
cd controllers
touch auth.js

Let’s create two methods signin and signup on our auth.js file.

const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const {
createJWT,
} = require("../utils/auth");
exports.signup = (req, res, next) => {
let { name, email, password, password_confirmation } = req.body;
User.findOne({email: email})
.then(user=>{
if(user){
return res.status(422).json({ errors: [{ user: "email already exists" }] });
}else {
const user = new User({
name: name,
email: email,
password: password,
});
bcrypt.genSalt(10, function(err, salt) { bcrypt.hash(password, salt, function(err, hash) {
if (err) throw err;
user.password = hash;
user.save()
.then(response => {
res.status(200).json({
success: true,
result: response
})
})
.catch(err => {
res.status(500).json({
errors: [{ error: err }]
});
});
});
});
}
}).catch(err =>{
res.status(500).json({
errors: [{ error: 'Something went wrong' }]
});
})
}
exports.signin = (req, res) => {
let { email, password } = req.body;
User.findOne({ email: email }).then(user => {
if (!user) {
return res.status(404).json({
errors: [{ user: "not found" }],
});
} else {
bcrypt.compare(password, user.password).then(isMatch => {
if (!isMatch) {
return res.status(400).json({ errors: [{ password:
"incorrect" }]
});
}
let access_token = createJWT(
user.email,
user._id,
3600
);
jwt.verify(access_token, process.env.TOKEN_SECRET, (err,
decoded) => {
if (err) {
res.status(500).json({ erros: err });
}
if (decoded) {
return res.status(200).json({
success: true,
token: access_token,
message: user
});
}
});
}).catch(err => {
res.status(500).json({ erros: err });
});
}
}).catch(err => {
res.status(500).json({ erros: err });
});
}

Let’s create a utils folder and add auth.js file in it where we will make a method to sign a jwt token including our payload, expiry time, and token secret.

const jwt = require("jsonwebtoken");exports.createJWT = (email, userId, duration) => {
const payload = {
email,
userId,
duration
};
return jwt.sign(payload, process.env.TOKEN_SECRET, {
expiresIn: duration,
});
};

Sign up Logic

  • Check if the user exists or not, if the user already exists, throw errors with the message email already exists.
  • If the user is a new user, use bcrypt to hash the password before storing it in your database
  • Save data(name, email, and password) in MongoDB.

Sign in Logic

  • Check if the user exists or not, if user not exists, throw errors with the message user not found.
  • If the user exists, we are checking whether the assigned and retrieved passwords are the same or not using the bcrypt.compare() method.
  • Sign our jwt and set the JWT token expiration time. Token will be expired within the defined duration which is 1hr in our current code.
  • If succeed send the token in our response with success status(200) and user information.

Setting up form validation in our express APIs

Next, we will implement validation in Express auth API using POST body request. We won’t use any dependencies for input validation instead we will validate every request and push into errors array. Our final auth.js file on controllers folder will look like this.

const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const {
createJWT,
} = require("../utils/auth");
const emailRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;exports.signup = (req, res, next) => {
let { name, email, password, password_confirmation } = req.body;
let errors = [];
if (!name) {
errors.push({ name: "required" });
}
if (!email) {
errors.push({ email: "required" });
}
if (!emailRegexp.test(email)) {
errors.push({ email: "invalid" });
}
if (!password) {
errors.push({ password: "required" });
}
if (!password_confirmation) {
errors.push({
password_confirmation: "required",
});
}
if (password != password_confirmation) {
errors.push({ password: "mismatch" });
}
if (errors.length > 0) {
return res.status(422).json({ errors: errors });
}
User.findOne({email: email})
.then(user=>{
if(user){
return res.status(422).json({ errors: [{ user: "email already exists" }] });
}else {
const user = new User({
name: name,
email: email,
password: password,
});
bcrypt.genSalt(10, function(err, salt) { bcrypt.hash(password, salt, function(err, hash) {
if (err) throw err;
user.password = hash;
user.save()
.then(response => {
res.status(200).json({
success: true,
result: response
})
})
.catch(err => {
res.status(500).json({
errors: [{ error: err }]
});
});
});
});
}
}).catch(err =>{
res.status(500).json({
errors: [{ error: 'Something went wrong' }]
});
})
}
exports.signin = (req, res) => {
let { email, password } = req.body;
let errors = [];
if (!email) {
errors.push({ email: "required" });
}
if (!emailRegexp.test(email)) {
errors.push({ email: "invalid email" });
}
if (!password) {
errors.push({ passowrd: "required" });
}
if (errors.length > 0) {
return res.status(422).json({ errors: errors });
}
User.findOne({ email: email }).then(user => {
if (!user) {
return res.status(404).json({
errors: [{ user: "not found" }],
});
} else {
bcrypt.compare(password, user.password).then(isMatch => {
if (!isMatch) {
return res.status(400).json({ errors: [{ password:
"incorrect" }]
});
}
let access_token = createJWT(
user.email,
user._id,
3600
);
jwt.verify(access_token, process.env.TOKEN_SECRET, (err,
decoded) => {
if (err) {
res.status(500).json({ erros: err });
}
if (decoded) {
return res.status(200).json({
success: true,
token: access_token,
message: user
});
}
});
}).catch(err => {
res.status(500).json({ erros: err });
});
}
}).catch(err => {
res.status(500).json({ erros: err });
});
}

Here we validate every input form data like empty fields, email type, password mismatch and if there are any errors then we returnerrors objects on response.

Update a index.js file in the root project’s folder and paste the following code in it.

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
require('dotenv').config();
//import routes
const authRoutes = require('./routes/auth');
const { db } = require('./models/User');
//app
const app = express();
// db
mongoose
.connect(process.env.DATABASE,{
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true
})
.then(() => console.log('DB Connected'));
//middlewares
app.use(bodyParser.json());
app.use(cors());
//routes middleware
app.use('/api', authRoutes);
const port = process.env.PORT || 8000;app.listen(port, () => {
console.log(`Server is running on ${port}`)
});

Testing our Sign up endpoint using Postman:

Signup API endpoint test on POSTMAN

Congrats we have successfully completed signup API. You can check on the MongoDB database for confirmation where you can see data you just created.

Let’s test validation errors for sign up endpoint.

Returning errors object as validation test on sign up endpoint

Whenever we miss any input field we will see our errors object returned. We can play with various conditions to check errors objects.

Testing our Sign in endpoint using Postman:

Sign In endpoint test on POSTMAN

We have successfully returned our token on sign-in endpoint with status 200 and user information. Lets test validation on the sign-in endpoint.

Sign in password incorrect validation

We can see password incorrect errors on our response while testing validation error on our sign-in endpoint. You can play around with different cases and see errors object accordingly.

Conclusion

Finally, we have completed our backend part where we created secure Token-Based Authentication REST API. So far, In this tutorial, we have learned how to securely store the password in the database using the hash method with bcrypt , how to create a JWT token to communicate with the client and a server using jsonwebtoken. We also implemented custom input validation and successfully tested endpoints which we will use on our frontend.

In Part 2, we’ll create our frontend using React, react-hook for state management and begin to use axios to fetch data from our server.

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

--

--

Anand Shrestha
The Startup

Software Developer | Web dev | Game dev |front-end React.js | Backend Nodejs | Unity3D