In a MERN (MongoDB, Express, React, Node.js) stack application, middleware functions play a crucial role in handling various aspects of request processing. One such critical middleware is to check login state, which is responsible for verifying user authentication through JSON Web Tokens (JWT). This article will delve into the checklogin middleware, explaining its code in detail and discussing why certain parts are implemented and separated into different files.


Code Overview

  1. auth.middleware.js

Create a file named auth.middleware.js in you middlewares folder. This file contains the checklogin middleware function. This function checks the validity of the JWT provided in the request headers.

require('dotenv').config();
const jwt = require('jsonwebtoken');
const userService = require('../modules/users/user.service');
const authService = require('../modules/auth/auth.service');

const checklogin = async (request, res, next) => {
	try {
		// Retrieves the authorization token from the request headers or sets it to null if not present.
		let token = request.headers["authorization"] || null;
		
		// Throws an error if the token is not provided in the request.
		if (!token) {
			throw { status: 400, message: "Token expected" };
		}

		// Splits the token and takes the actual token part.
		// When a client sends an authorization token in the HTTP headers, it often follows a specific format as authorization: Bearer <token>
		//splits the token string into an array of substrings based on the space character (" ") & .pop() removes and returns the last element of the array i.e. the <token> value
		token = token.split(" ").pop();
		
		// Retrieves the personal access token (PAT) data from the database
		const pat = await authService.getPATDATA({ accessToken: token });

		if (pat) {
			// Verifies the token using the secret key from environment variables and decodes the payload.
			const data = jwt.verify(token, process.env.JWT_SECRETE);

			// Checks if a user exists in the database with the user ID from the token payload.
			const userExists = await userService.getSingleUserByFilter({
				_id: data.sub
			});

			// Throws an error if the user does not exist in the database.
			if (!userExists) {
				throw { status: 401, message: "User does not exist anymore" };
			} else {
				// Attaches the user details to the request object if the user exists.
				request.authUser = {
					_id: userExists._id,
					name: userExists.name,
					email: userExists.email,
					address: userExists.address,
					phone: userExists.phone,
					role: userExists.role,
					image: userExists.image,
					userprovider: userExists.userprovider,
					userproviderID: userExists.userproviderID,
				};
				request.currentSesson = pat;
				next();
			}
		} else {
			throw { status: 401, message: "Access token does not exist" };
		}

	} catch (exception) {
		// Passing the error to the next error-handling middleware with status and message.
		console.log("Middleware | checklogin | Error");
		console.log(exception);
		next({ status: exception.status || 401, message: exception.message });
	}
};

module.exports = checklogin;
  1. user.service.js

The user.service.js file contains a service function to interact with the user database. The getSingleUserByFilter function retrieves a user based on a given filter.

// Function to retrieve a single user based on a provided filter
const getSingleUserByFilter = async (filter) => {
	try {
		// Queries the database to find one user that matches the filter criteria
		const user = await UserModel.findOne(filter);
		// Returns the found user
		return user;
	} catch (exception) {
		// Throws any exception that occurs during the query
		throw exception;
	}
};

module.exports = { getSingleUserByFilter };
  1. auth.service.js

The auth.service.js file contains a service function to interact with the Personal Access Token (PAT) data. The getPATDATA function retrieves PAT details based on a given filter.

// Function to retrieve personal access token (PAT) data based on a provided filter
const getPATDATA = async (filter) => {
	try {
		// Queries the database to find one PAT that matches the filter criteria
		const pat = await PatModel.findOne(filter);
		// Returns the found PAT data
		return pat;
	} catch (error) {
		// Throws any error that occurs during the query
		throw error;
	}
};

module.exports = { getPATDATA };
  1. pat.model.js

The pat.model.js file defines the schema and model for storing personal access tokens in the database.

const mongoose = require('mongoose');
const PatSchema = new mongoose.Schema({
	// User ID reference
	userId: {
		type: mongoose.Types.ObjectId,
		ref: "User",
		required: true
	},
	
	// The access token string
	accessToken: {
		type: String,
		required: true
	},
	
	// The refresh token string
	refreshToken: {
		type: String,
		required: true
	}
}, {
	// Automatically adds createdAt and updatedAt timestamps to the schema
	timestamps: true,
	// Automatically creates indexes to improve query performance
	autoIndex: true,
	// Automatically creates the collection in the database if it doesn't exist
	autoCreate: true
});

const PatModel = mongoose.model("PAT", PatSchema);
module.exports = PatModel;

Here, a refresh token is used to obtain a new access token without requiring the user to re-authenticate. This helps maintain a seamless user experience by allowing continuous access even after the original access token expires. Refresh tokens enhance security by reducing the need to store long-lived access tokens.

Explanation and Notes

Why Use Middleware for Authentication?