User Authentication in MERN Stack — Part 1 (Backend)
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
andlogin
usingjsonwebtoken
for 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 8000
on 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:
- Request URL: http://localhost:8000/api/signup
- Set method Type to
POST
- Switch to body tab, choose raw, fill data in JSON objects, and hit send.
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.
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:
- Request URL: http://localhost:8000/api/signin
- Set method Type to
POST
- Switch to body tab, choose raw, fill data in JSON objects, and hit send.
We have successfully returned our token on sign-in endpoint with status 200 and user information. Lets test validation on the sign-in endpoint.
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!