PART-2: User Authentication with JWT and Express.js in Node.js: A JOIful Journey

Vivekumar08
9 min readNov 12, 2023

--

Welcome back to the JOIful Journey of building user authentication with JWT and Express.js in Node.js! If you’ve followed along with us in Part 1, you’ve already set up a fresh Node.js project, installed the necessary packages, and organized your project structure, and routes that will handle user registration & login.

Now, it’s time to dive into the next chapter where we’ll add even more flavor to our authentication recipe. Get ready for Step 8, where we’ll define the middleware, controllers, services, and maybe a few other goodies.

But before we jump into the code, let’s take a moment to appreciate the groundwork we’ve laid so far. Our project is shaping up, and the excitement is building. So, grab your favorite beverage, fire up your code editor, and let’s continue this JOIful journey together! 🚀

Photo by Fab Lentz on Unsplash

Let’s get started

Step 8: Middleware

  1. Database Connection Middleware

Create middleware/db.js file to define the database connection middleware:

  • It checks if a connection already exists (dbConnection). If it does, it proceeds to the next middleware. If not, it establishes a new connection.
  • If the connection fails, it sends an error response.
require('dotenv')
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
let dbConnection;

const options = {
//NEW CONFIGURATION
keepAlive: true,
useNewUrlParser: true,
useUnifiedTopology: true,
connectTimeoutMS: 200000,
socketTimeoutMS: 2000000,
keepAlive: true,
useNewUrlParser: true,
dbName: process.env.DB_NAME
};

exports.connectToDatabase = async (req, res, next) => {
if (dbConnection) {
console.log('----DB----PREVIOUS-CONNECTION----------------');
next()
} else {
console.log("process.env.DB_STRING, options ", process.env.DB_STRING);
mongoose.connect(process.env.DB_STRING, options)
.then(db => {
console.log('----DB----NEW-CONNECTION----------------');
dbConnection = db.connections[0].readyState;
console.log('----DB----NEW-CONNECTION-INIT----------------');
next()
},
err => {
console.log('----DB----ERROR-CONNECTION----------------');
console.log(err);
return res.send({
status_code: 409,
success: false,
message: 'DB connection failure'
});
}
);
}
};

2. Custom Error Handling Middleware

Create middleware/error-handler.js file to define the error types middleware:

  • This middleware handles errors of type GeneralError and its subclasses (BadRequest, NotFound, Unauthorized, ApplicationError, InsufficientAccessError).
  • It sends a JSON response with the appropriate status code and error message.

3. Response Handlers

Create middleware/response-handler.js file to define the custom error responses as per requirement middleware:

  • responseHandler: Sends a JSON response with a specified status code, message, and data.
  • clientHandler: Sends a JSON response with a specified status code, message, and null data.
  • errorHandler: Sends a JSON response with a specified status code and error message.
module.exports.responseHandler = (data, res, message, status) => {
const statusCode = status || 200;
res.status(statusCode).json({
status: statusCode || 200,
message: message || 'Success',
data: data
})
}
module.exports.clientHandler = (message, status, res) => {
const statusCode = status || 400;
res.status(statusCode).json({
status: statusCode || 400,
message: message || 'Failure',
data: null
})
}

module.exports.errorHandler = (status, res, message) => {
const statusCode = status || 500;
res.status(statusCode).json({
status: 'error',
message: message || `Error`
})
}

4. Validation Middleware (validate)

Create middleware/validator.js file to define the JOI to validate the request body against a given schema.

  • If there are validation errors, it returns a response using the responseHandler from the middlewares module.
const { responseHandler } = require("../middlewares/response-handler");

const defaults = {
'abortEarly': false, // include all errors
'allowUnknown': true, // ignore unknown props
'stripUnknown': true // remove unknown props
};

const validate = (schema) => {
return (req, res, next) => {
const {error, value} = schema.validate(req.body, defaults);
if(error){
return responseHandler(error, res, error.message, 422);
}
req.value = value;
next();
}
};


module.exports = validate;

5. Auth middleware

Create middleware/auth.js file to define the database connection middleware:

  • import important packages on headers
const jwt = require("jsonwebtoken");
const { responseHandler } = require("../middlewares/response-handler")
const refreshTokenService = require("../service/auth/refreshToken");
require('dotenv').config();

verifyRefreshToken function:

  • This middleware is designed to be placed in the middleware stack to verify the validity of a refresh token.
  • It checks if the Authorization header is present in the request. If not, it returns an unauthorized response.
  • If the header is present, it extracts the token and checks it against the database (using refreshTokenService.getToken), and ensures its validity using jwt.verify.
  • If the token is invalid or not found in the database, it returns an unauthorized response.
  • If everything is valid, it decodes the token and attaches the user information to the request (req.user) for further use in subsequent middleware or route handlers.
exports.verifyRefreshToken = async (req, res, next) => {
try {
var headerToken = req.headers.authorization;
//if no token found, return response (without going to the next middelware)
if (!headerToken) {
return responseHandler(null, res, 'Unauthorized!', 401);
}

if (headerToken.includes("Bearer")) {
headerToken = headerToken.substr(7);
}

const token = await refreshTokenService.getToken({ token: headerToken });

if (!token) {
return responseHandler(null, res, "Invalid Refresh Token!", 401);
};

const decoded = jwt.verify(headerToken, process.env.REFRESH_TOKEN_PRIVATE_KEY);
req.user = decoded;
next();

} catch (err) {
console.log("error is ", err);
return responseHandler(null, res, err, 401);
}
};

accessToken function

  • It checks if the Authorization header is present. If not, it returns an unauthorized response.
  • It extracts the token, verifies it using jwt.verify, and attaches the decoded user information to the request if the token is valid.
  • If the token is invalid, it returns an access denied response.
exports.verifyAccessToken = async (req, res, next) => {
try {

var token = req.headers.authorization;
//if no token found, return response (without going to the next middelware)
if (!token) {
return responseHandler(null, res, 'Unauthorized!', 401);
}

if (token.includes("Bearer")) {
token = token.substr(7);
}

const decoded = jwt.verify( token, process.env.ACCESS_TOKEN_PRIVATE_KEY);

req.user = decoded;
next();

} catch (err) {
console.log("error is ", err);
return responseHandler(null, res, "Access Denied: Invalid token", 500);
}
}

NOTE:-

  • It’s a good practice to handle errors consistently. In the catch block, you are logging the error but returning a response with a 401 status code. It might be better to log the error and then send a more generic error response (e.g., 500 Internal Server Error) to the client to avoid exposing too much information about the error.
  • Ensure that the private keys for JWT (process.env.REFRESH_TOKEN_PRIVATE_KEY and process.env.ACCESS_TOKEN_PRIVATE_KEY) are securely stored and not exposed in your code.

Step 9: Services provided to MongoDB Schema

  • Create services/auth/user.js file to define all possible services for users
const model = require('../../models/auth/user');
const refreshTokenModel = require("../../models/auth/refreshToken");
const dal = require('../../dal/dal');
const bcrypt = require('bcryptjs');
const utils = require('../../utils/utils');
require('dotenv').config();


exports.findOne = async (filter) => {
return await dal.findOne(model, filter, {});
};

exports.addUser = async (value) => {
let token;
value.password = await bcrypt.hash(value.password, 10);
value.userName = utils.generateUsername(value.name)
let count
if (value.email) {
count = await dal.findOne(model, { email: value.email }, { email: 1 })
if (count) {
return { userData: null, token: null, message: "User Already exist with this email" }
}
} else if (value.phone) {
count = await dal.findOne(model, { phone: value.phone }, { phone: 1 })
if (count) {
return { userData: null, token: null, message: "User Already exist with this phone" }
}
} else {
return { userData: null, token: null, message: "Invalid mode to signup" }
}

const data = await dal.create(model, value)
const body = {
id: data._id,
userName: data.userName,
name: data.nama,
email: data?.email || null,
phone: data?.phone || null,
};
token = utils.getAccessToken(body);
let refreshToken = utils.getRefreshToken(body);

let refreshBody = {
userId: data._id,
token: refreshToken
}

await dal.create(refreshTokenModel, refreshBody);

return {
userData: body, token: token,
refreshToken: refreshToken,
}
}

exports.login = async (value) => {
let token;
const projections = {
userName: 1,
name: 1,
email: 1,
phone: 1,
password: 1,
}
let user;

if (value.mode === "email") {
user = await dal.findOne(model, { email: value.email }, projections)
} else if (value.mode === "phone") {
user = await dal.findOne(model, { phone: value.phone }, projections)

} else {
return { userData: null, token: null, message: "Incorrect mode for login" }
}

if (!user) {
return { userData: null, token: null }
};
if (value.password) {
const result = await bcrypt.compare(value.password, user.password);
if (!result) return { userData: null, token: null, message: "Please double check the credentials" }
}
const userData = {
id: user._id,
userName: user?.userName,
name: user?.name,
phone: user?.phone,
email: user?.email,
}

token = utils.getAccessToken(userData)
let refreshToken = utils.getRefreshToken(userData)

let refreshBody = {
userId: user._id,
token: refreshToken
}
let tokenData
tokenData = await dal.findOne(refreshTokenModel, { userId: user._id })
if (tokenData) return { userData, token: null, message: "USER ALREADY LOGIN" }

await dal.create(refreshTokenModel, refreshBody)


return {
userData,
token: token,
refreshToken: refreshToken
}
}

exports.logout = async (filter) => {
const data = await dal.findOneAndDelete(refreshTokenModel, filter)
if (!data) return { message: "Already Logout", status: 400 }
return { message: "Logout successful", status: 200 }
}
  • Create services/auth/refreshToken.js file to define services related to refresh tokens that called inside the middleware
const model = require("../../models/auth/refreshToken");
const dal = require("../../dal/dal");
const jwt = require("jsonwebtoken");
require('dotenv').config();

exports.getToken = async (filter, projection = {}) => await dal.findOne(model, filter, projection);

let generateAccessToken = (body) => {
return jwt.sign(body, process.env.ACCESS_TOKEN_PRIVATE_KEY, { expiresIn: process.env.ACCESS_TOKEN_EXPIRY_DAY })
}

exports.generateAccessToken = async (data) => {

const body = {
id: data.id,
userName: data?.userName || null,
name: data.nama,
email: data?.email || null,
phone: data?.phone || null,
active: data.active
}

const accessToken = generateAccessToken(body);
return accessToken;
};

exports.deleteToken = async (filter) => {
return await dal.findOneAndDelete(model, filter);
}

Step 10: Controllers for users and refreshTokens

  • Create controllers/auth/user.js to define all possible actions that can be triggered by user initially
const service = require('../../service/auth/user');
const { responseHandler, clientHandler } = require("../../middlewares/response-handler");

exports.signup = async (req, res, next) => {
try {
const value = req.value;
const { userData, token, refreshToken, message } = await service.addUser(value);
if (message) return clientHandler(null, res, message, 400);
if (!userData) return clientHandler(null, res, 'No user', 400);
const data = {
data: userData,
accessToken: token,
refreshToken: refreshToken
};
responseHandler(data, res);
} catch (error) {
console.error(error);
next(error);
}
}

exports.getUser = async (req, res, next) => {
try {
const { userId } = req.user
const data = await service.findOne({ _id: userId })
delete data._doc["password"]
responseHandler(data, res);
} catch (error) {
console.error(error);
next(error);
}
}

exports.login = async (req, res, next) => {
try {

const value = req.value;

const { userData, token, refreshToken, message } = await service.login(value);

if (message) return responseHandler(userData || null, res, message, 400);

if (!userData) return responseHandler(null, res, 'No user', 400);

if (!token) return responseHandler(null, res, 'Invalid email OR password!', 400);

const data = {
data: userData,
accessToken: token,
refreshToken: refreshToken
};

responseHandler(data, res);
} catch (error) {
console.error(error);
next(error);
}
};

exports.logout = async (req, res, next) => {
try {
const filter = { userId: req.user.id }
const data = await service.logout(filter)
responseHandler(data.message, res, data.message, data.status)
} catch (error) {
console.error(error);
next(error);
}
}
  • Create controllers/auth/refreshToken.js to define actions for the refresh token
const service = require("../../service/auth/refreshToken");
const { responseHandler } = require("../../middlewares/response-handler");
const mongoose = require('mongoose')
require('dotenv').config();

exports.getAccessToken = async (req, res, next) => {
try {

const user = req.user;

const accessToken = await service.generateAccessToken(user);

responseHandler({ accessToken: accessToken }, res);

} catch (error) {
console.error("error is ", error);
next(error);
}
};

exports.deleteRefreshToken = async (req, res, next) => {
try {

const userId = req.params.userId;

const token = await service.getToken({ userId: new mongoose.Types.ObjectId(userId) });

if (!token) {
return responseHandler(null, res, "something went wrong, unable to delete token!", 401);
};

return responseHandler(null, res, "Token Deleted Successfully!", 200);

} catch (error) {
console.error("error is ", error);
next(error);
}
}

Step 11: Start the server

Before going to the terminal, make sure you’re in the login-authentication directory and configure package.json start script:

"scripts": {
"start": "nodemon server.js",
"test": "echo \"Error: no test specified\" && exit 1"
}

and run the command in the terminal

npm start

Your Express server should be running on port 5000. You can now make requests to the authentication endpoints (/api/v1/user/signup and /api/v1/user/login) using a tool like Postman.

NOTE: Make sure your .env file should look like this

#db
DB_NAME= dbName
DB_STRING = mongo-url

#jwt
REFRESH_TOKEN_PRIVATE_KEY = your-refresh-token-private-key
ACCESS_TOKEN_PRIVATE_KEY = your-accesstoken-private-key
JWT_SECRET=skeleton-nodejs-user-authentication#jwt
ACCESS_TOKEN_EXPIRY_DAY= 1h
REFRESH_TOKEN_EXPIRY_DAY = 30d

PORT = 5000

Conclusion

And there you have it! You’ve successfully set up the foundation of our authentication system, added some middleware magic, and even set the stage for handling errors gracefully. If you’ve followed along smoothly, kudos to you on this JOIful Journey so far!

But hey, we know the coding path can sometimes be a twisty one. If you’ve hit a bump or can’t find that elusive file, fear not! Head over to my GitHub repository for this project. There, you’ll find the complete codebase, each step neatly organized, and maybe a surprise or two.

Remember, the coding adventure is all about learning and enjoying the journey. So, if you’re ready to tackle the next challenges or just want to explore the code from a different angle, dive into the repository and keep that JOIful momentum rolling!

GitHub Repository Link: User-Authentication

Stay JOIful, fellow coder! 🚀

--

--

Vivekumar08

A passionate Developer with innovative ideas into reality. With an experience in the MERN stack and cutting-edge Nextjs technology, dynamic approach to web dev.