Mastering User Authentication: MERN Stack Login Page (Part 1 — Backend)
Introduction
Welcome to my guide on creating a user-friendly login page using the MERN stack: MongoDB, Express.js, React, and Node.js. This tutorial will be divided into two parts: the initial blog post will focus on setting up the backend, while the subsequent blog post will demonstrate connecting the backend to the frontend and focus on creating the UI using React.
In today’s digital landscape, a secure login page is an essential component of any application, serving as the primary gateway for users to access their accounts. This tutorial assumes you have Node.js and React installed and possess a basic understanding of the MERN stack. However, if you’re new to these technologies, you can refer to the Node.js installation guide for assistance.
The MERN stack offers a robust suite of technologies, empowering developers to build dynamic web applications. Throughout this tutorial, we’ll delve into crucial aspects required to develop a simple yet secure login page. From establishing the backend infrastructure using Node.js and Express.js to integrating the frontend with React, we’ll explore essential authentication methodologies using tools such as crypto for password encryption and JWT for robust user authentication.
Join me on this journey to master user authentication with the MERN stack!
What is MERN Stack?
The MERN stack comprises four key components:
- MongoDB: A NoSQL database that stores data in JSON-like documents. MongoDB is highly scalable and offers flexibility due to its schemaless nature, making it ideal for storing large volumes of data in various formats. For our project, we’ll utilize MongoDB Atlas as our database solution. MongoDB Atlas is a cloud-based database service that provides a managed MongoDB environment, offering flexibility, scalability, and security while handling data storage and retrieval seamlessly.
- Express.js: A minimalist web application framework for Node.js. Express simplifies building robust APIs and web applications by providing essential features for routing, handling requests, and managing middleware.
- React: A JavaScript library for building interactive user interfaces. React enables the creation of dynamic, reusable UI components, enhancing the efficiency of front-end development.
- Node.js: A server-side JavaScript runtime that executes JavaScript code outside the browser. Node.js facilitates building scalable and high-performance applications, allowing JavaScript to be used for both front-end and back-end development.
File Structure
Before we delve into creating our login page, let’s take a moment to understand the structure of our project. This understanding will help you navigate through the files and directories as we progress through the tutorial
The file structure in backend development plays a pivotal role in organizing a manageable and scalable codebase. In a typical MERN setup, you’ll often encounter three main directories:
- Models Directory: This directory holds essential schemas defining the data structure within our application.
- Routes Directory: Here, you’ll find the definition of various endpoints or routes, governing how the application reacts to incoming client requests. Each route is linked to specific controller functions based on a particular URL and HTTP method.
- Controllers Directory: This directory houses the logic associated with different routes. It contains functions responsible for handling requests, processing data, communicating with models, and formulating responses back to the client.
Setting up and installing the libraries
1. Initializing the Project
To initiate a Node.js project, access your preferred directory in the terminal and execute the following command:
npm init -y
Note: If you prefer customizing the project description, omit the -y
flag and follow the prompts. The above command generates a package.json
file, serving as a project manifest, containing metadata and dependencies.
2. Library Installation
Utilize npm install <package-name>@<version>
to install libraries. For instance, to install Express, execute:
npm install express@^4.18.2
Here are the libraries, we will be using for our project:
Creating a Database with MongoDB Atlas
To set up our database, we’ll be using MongoDB Atlas, a fully managed cloud database service provided by MongoDB. Follow the instructions provided in the MongoDB Atlas documentation to create and configure your database.
We will be using the connection string in the last step in our application.
Implementation
1. Models
Despite MongoDB being a NoSQL database, integrating Mongoose with MongoDB empowers us to establish structure and apply constraints to our data, fostering security and consistency within the system. Alongside constraints, Mongoose allows us to define functions within our schema and utilize virtual to compute or derive new properties from existing data without physically persisting them in the database.
You can learn more about Mongoose from the official documentation Mongoose documentation
The following schema is designed to define the structure for user data, incorporating stringent validations to ensure enhanced security and coherence within the database. In this schema, we’ll outline how to generate a UUID as our encryption key and utilize the SHA256 hashing technique via the crypto library to encrypt our passwords.
Directory: LoginPageApplication/Backend/Models/UserModel.js
// Importing necessary modules
const mongoose = require('mongoose')
var crypto = require("crypto");
const {v4: uuidv4} = require('uuid')
//Defining the User Schema
const userSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
maxLength: 30,
minLength: 2,
},
email: {
type: String,
trim: true,
required: true,
unique: true
},
encrypted_password: {
type: String,
required: true
},
salt: String,
},
{timestamps: true}
);
//Creating a "virtua" field that will take in password and encrypt it
userSchema.virtual("password")
.set(function(password){
this._password = password;
this.salt = uuidv4();
this.encrypted_password = this.securedPassword(password);
})
.get(function(){
return this._password
})
//Defining some methods associated with user schema
userSchema.method({
//To check if the password is correct
authenticate: function(plainpassword){
return this.securedPassword(plainpassword) === this.encrypted_password
},
//To encrpty the password
securedPassword: function(plainpassword){
if(!plainpassword) return "";
try{
return crypto.createHmac('sha256', this.salt)
.update(plainpassword)
.digest('hex')
}
catch(err){
return "Error in hashing the password";
}
},
})
//Export the userSchema as User
module.exports = mongoose.model("User", userSchema);
2. Controllers
Now, let’s define essential authentication functions within our authentication controller.
signup
Function: Thesignup
function handles the registration of new users. It integrates express-validator to validate incoming user input, ensuring the necessary fields' presence and accuracy. Validation constraints will be refined later; currently, we're focusing on retrieving and managing the validation outcomes.signin
Function: In thesignin
function, our goal is to locate the user within the database. If found, it establishes user details as cookies within the browser and generates a JWT (JSON Web Token) for secure data transmission between server and client. If the user isn't found, an appropriate error message is returned.signout
Function: This function clears the user's JWT token stored as a browser cookie, effectively ending the user's session. After successful sign-out, it sends a confirming JSON response.isSignedIn
andisAuthenticated
Functions: Within our authentication module, these two functions serve distinct purposes:- •
isSignedIn
: Validates the presence of tokens required for authentication. isAuthenticated
: Confirms a user's authorization to perform specific actions, preventing unauthorized access to certain functionalities or data.
Directory: LoginPageApplication/Backend/Models/AuthController.js
// Importing necessary modules and models
const User = require("../models/userModel");
const { check, validationResult } = require("express-validator");
const jwtToken = require('jsonwebtoken');
const { expressjwt: jwt } = require("express-jwt");
// SIGNUP: Registering a new user
exports.signup = (req, res) => {
// Validate user input using express-validator
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: errors.array()[0].msg,
});
}
// Creating a new user instance and saving it to the database
const user = new User(req.body);
user.save()
.then(user => {
res.json({
id: user._id,
name: user.name,
email: user.email,
});
})
.catch(err => {
let errorMessage = 'Something went wrong.';
if (err.code === 11000) {
errorMessage = 'User already exists, please signin';
}
return res.status(500).json({ error: errorMessage });
});
};
// SIGNIN: Authenticating existing user
exports.signin = async (req, res) => {
// Validate user input using express-validator
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: errors.array()[0].msg,
});
}
// Checking user credentials and generating JWT token for authentication
const { email, password } = req.body;
await User.findOne({ email: `${email}` })
.then(user => {
if (!user) {
return res.status(400).json({
error: "User not found"
});
}
if (!user.authenticate(password)) {
return res.status(401).json({
error: "Email or Password does not exist"
});
}
// Setting JWT token as a cookie in the browser
const token = jwtToken.sign({ _id: user._id }, 'shhhhh');
res.cookie("token", token, { expire: new Date() + 9999 });
const { _id, name, email } = user;
return res.json({ token, user: { _id, name, email } });
});
};
// SIGNOUT: Clearing user token
exports.signout = (req, res) => {
res.clearCookie("token");
res.json({
message: "User has signed out"
});
};
// Protected Routes
exports.isSignedIn = jwt({
secret: 'shhhhh',
userProperty: "auth",
algorithms: ['HS256']
});
exports.isAuthenticated = (req, res, next) => {
let checker = req.profile && req.auth && req.profile._id == req.auth._id;
if (!checker) {
return res.status(403).json({
error: "ACCESS DENIED"
});
}
next();
};
3. Routes
Within our Express.js routes lies the core functionality of user authentication, meticulously handling various operations:
- Signup Route: Designed to facilitate user registration, this route employs express-validator to rigorously validate essential user input. Prior to processing the signup request, it ensures the accuracy and completeness of crucial fields.
- Signin Route: Orchestrates user login operations by validating user credentials against the database. This route not only establishes user details as browser cookies but also issues a JWT (JSON Web Token) for secure data transmission. In case of login failures, it responds with precise error messages.
- Signout Route: Manages user session termination by clearing the JWT token stored as a browser cookie. Upon successful sign-out, it promptly confirms the user’s logout with a JSON response.
- Protected Routes: These routes leverage middleware functions like
isSignedIn
andisAuthenticated
to validate user authentication and authorize specific actions. They serve as a protective shield, preventing unauthorized access to privileged resources and functionalities.
Understanding the diverse functions of distinct HTTP request types:
- GET: Fetching data from a specified resource.
- POST: Submitting data for processing to a specified resource.
- PUT: Executing update operations on a specified resource.
- DELETE: Removing data from a specified resource.
Directory: LoginPageApplication/Backend/Routes/AuthRoute.js
// Importing necessary modules and models
const express = require("express");
var router = express.Router();
const { check } = require('express-validator');
const { signin, signup, signout, isSignedIn } = require("../controllers/authController");
// POST request for user signup
router.post(
"/signup",
[
// Validation for name, email, and password
check("name", "Name must be 3+ chars long").isLength({ min: 3 }),
check("email", "Email is required").isEmail(),
check("password", "Password must contain 8+ chars").isLength({ min: 8 })
],
signup // Call the signup function from the authController
);
// POST request for user signin
router.post(
"/signin",
[
// Validation for email and password
check("email", "Email is required").isEmail(),
check("password", "Password is required").isLength({ min: 1 })
],
signin // Call the signin function from the authController
);
// GET request for user signout
router.get("/signout", signout);
// Protected Route for testing
router.get("/testroute", isSignedIn, (req, res) => {
res.send("A protected route");
});
module.exports = router; // Export the router module
4. Bringing it all together in index.js
- Database Connection: Establishes a connection to MongoDB using Mongoose. The
mongoose.connect()
method connects to a MongoDB cluster and logs the connection status. - Starting the Application: Starts the Express application on port 8000 using the
app.listen()
method and logs a message indicating the server's status. - Middleware Configuration: Utilizes middleware for parsing incoming requests. It uses
bodyParser.json()
to parse JSON data,cookieParser()
to handle cookies, andcors()
to enable Cross-Origin Resource Sharing, enhancing the security and functionality of the application. - Routing: Defines routes for the application. In this code snippet, the
Authroute
variable (presumably containing authentication-related routes) is specified to be used under the/api
endpoint.
//Importing necessary modules and models
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const mongoose = require('mongoose');
const Authroute = require("./routes/authRoute")
// Database Connection
// Replace <username>, <password>, <cluster>, and <dbname> with your MongoDB Atlas credentials.
mongoose.connect("mongodb+srv://<username>:<password>@<cluster>/<dbname>?retryWrites=true&w=majority")
.then(() => console.log("Database connected successfully"))
.catch((err) => console.log("Database connection failed", err));
// Starting the Application
const port = 8000;
app.listen(port, () => {
console.log(`App is running at ${port}`);
});
// Middleware Configuration
// Body-parser to parse incoming request bodies as JSON
app.use(bodyParser.json());
// Cookie-parser for handling cookies
app.use(cookieParser());
// CORS for enabling Cross-Origin Resource Sharing
app.use(cors());
// Routing
// Mounting authentication-related routes under the '/api' endpoint
app.use("/api", Authroute);
Conclusion
With the completion of our backend setup, we’ve laid a strong foundation for our application. In the next part (Part 2), we’ll dive into the exciting process of connecting our robust backend to the frontend using React. Additionally, we’ll explore the intricacies of building an interactive and user-friendly frontend interface.
If you’re interested, I’d be happy to guide you through the process of testing the backend routes using Postman. Let me know if exploring this further would be beneficial for your development journey!