Verify Authentication Middleware

The Verify Authentication Middleware protects private routes by validating user authentication using JWT access tokens and refresh tokens.

It automatically:

  • Verifies access tokens from cookies
  • Refreshes expired access tokens using refresh tokens
  • Re-attaches authenticated user data to the request
  • Responds with standardized API errors when authentication fails

This middleware is designed for secure, session-like authentication in REST APIs.

How Authentication Works

The middleware follows this sequence:

  1. Read accessToken and refreshToken from cookies
  2. If access token is valid → allow request
  3. If access token is expired:
    • Verify refresh token
    • Validate user existence
    • Issue new access & refresh tokens
    • Set new cookies automatically
  4. If all checks fail → reject the request

This ensures seamless token rotation without forcing the user to re-login.

Installation Guide

npx servercn-cli add verify-auth-middleware

Prerequisites

Ensure the following environment variables are defined in .env:

PORT = "3000";
NODE_ENV = "development";
LOG_LEVEL = "info";
JWT_ACCESS_SECRET = "your-access-secret";
JWT_REFRESH_SECRET = "your-refresh-secret";

Ensure the following configuration are defined:

MVC: src/configs/env.ts

Feature: src/shared/configs/env.ts

interface Config {
  PORT: number;
  NODE_ENV: string;
  LOG_LEVEL: string;
  JWT_REFRESH_SECRET: string;
  JWT_ACCESS_SECRET: string;
}
 
const env: Config = {
  PORT: Number(process.env.PORT) || 3000,
  NODE_ENV: process.env.NODE_ENV || "development",
  LOG_LEVEL: process.env.LOG_LEVEL || "info",
  JWT_REFRESH_SECRET: process.env.JWT_ACCESS_SECRET!,
  JWT_ACCESS_SECRET: process.env.JWT_ACCESS_SECRET!
};
 
export default env;

To ensure the authentication middleware functions correctly, your project must define a User model with a structure similar to the following.

MVC: src/models/user.model.ts

Feature: src/shared/modules/user/user.model.ts

import mongoose, { Document, Model, Schema } from "mongoose";
 
export interface IUser extends Document {
  _id: mongoose.Types.ObjectId;
  name: string;
  email: string;
  password: string;
  role: "user" | "admin";
  isEmailVerified: boolean;
 
  createdAt: Date;
  updatedAt: Date;
}
 
const userSchema = new Schema<IUser>(
  {
    name: {
      type: String,
      required: [true, "Name is required"],
      trim: true
    },
    email: {
      type: String,
      required: [true, "Email is required"],
      unique: true,
      lowercase: true,
      trim: true
    },
    password: {
      type: String,
      select: false,
      default: null
    },
    role: {
      type: String,
      enum: ["user", "admin"],
      default: "user"
    },
    isEmailVerified: {
      type: Boolean,
      default: false
    }
  },
  {
    timestamps: true
  }
);
 
const User: Model<IUser> = mongoose.model<IUser>("User", userSchema);
export default User;

To access authenticated user data inside request handlers, define a custom request type.

src/types/user.ts
import { Request } from "express";
import mongoose from "mongoose";
 
export interface UserRequest extends Request {
  user?: {
    _id?: string | mongoose.Types.ObjectId;
  };
}

Basic Implementation

src/middlewares/verify-auth.ts
import { NextFunction, Request, Response } from "express";
import {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken
} from "../utils/jwt";
import { UserRequest } from "../types/user";
import { ApiError } from "../utils/api-error";
import { logger } from "../utils/logger";
import User from "../models/user.model";
import env from "../configs/env";
 
const isProduction = env.NODE_ENV === "production";
const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000;
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;
 
export const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: isProduction,
  sameSite: isProduction ? ("none" as const) : ("lax" as const),
  path: "/"
};
 
export async function verifyAuthentication(
  req: UserRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const accessToken = req.cookies?.accessToken;
  const refreshToken = req.cookies?.refreshToken;
 
  // Step 1: Try validating access token
 
  try {
    if (accessToken) {
      const decoded = verifyAccessToken(accessToken);
      req.user = decoded;
      return next();
    }
  } catch (err) {
    // Access token expired or invalid
    logger.warn("Access token verification failed");
  }
 
  // Step 2: Refresh token required if access token fails
 
  if (!refreshToken) {
    return next(ApiError.unauthorized("Unauthorized, Please login first."));
  }
 
  try {
    const decodedRefresh = verifyRefreshToken(refreshToken);
 
    // Step 3: Ensure user still exists
 
    const userInDb = await User.findOne({
      _id: decodedRefresh.userId
    });
 
    if (!userInDb) {
      return next(ApiError.unauthorized("Unauthorized, Please login first."));
    }
 
    // Step 4: Issue new tokens
 
    const newAccessToken = generateAccessToken({
      _id: userInDb._id.toString()
    });
 
    const newRefreshToken = generateRefreshToken(decodedRefresh.userId);
 
    // Step 5: Saved accessToken and refreshToken in cookie
    res.cookie("accessToken", newAccessToken, {
      ...COOKIE_OPTIONS,
      maxAge: ACCESS_TOKEN_EXPIRY
    });
 
    res.cookie("refreshToken", newRefreshToken, {
      ...COOKIE_OPTIONS,
      maxAge: REFRESH_TOKEN_EXPIRY
    });
 
    // Step 6: Attach user to request
 
    req.user = {
      _id: decodedRefresh.userId
    };
 
    // you can update the refresh token in the database here if you store it in the database
 
    return next();
  } catch (err: any) {
    logger.warn("Refresh token verification failed");
    return next(ApiError.unauthorized("Unauthorized, Please login first."));
  }
}
src/shared/middlewares/verify-auth.ts
import {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken
} from "../utils/jwt";
import { logger } from "../utils/logger";
import env from "../configs/env";
import { UserRequest } from "../../types/user";
import { ApiError } from "../errors/api-error";
import User from "../../modules/user/user.model";
import { NextFunction, Response } from "express";
 
const isProduction = env.NODE_ENV === "production";
const ACCESS_TOKEN_EXPIRY = 15 * 60 * 1000;
const REFRESH_TOKEN_EXPIRY = 7 * 24 * 60 * 60 * 1000;
 
export const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: isProduction,
  sameSite: isProduction ? ("none" as const) : ("lax" as const),
  path: "/"
};
 
export async function verifyAuthentication(
  req: UserRequest,
  res: Response,
  next: NextFunction
): Promise<void> {
  const accessToken = req.cookies?.accessToken;
  const refreshToken = req.cookies?.refreshToken;
 
  // Step 1: Try validating access token
 
  try {
    if (accessToken) {
      const decoded = verifyAccessToken(accessToken);
      req.user = decoded;
      return next();
    }
  } catch (err) {
    // Access token expired or invalid
    logger.warn("Access token verification failed");
  }
 
  // Step 2: Refresh token required if access token fails
 
  if (!refreshToken) {
    return next(ApiError.unauthorized("Unauthorized, Please login first."));
  }
 
  try {
    const decodedRefresh = verifyRefreshToken(refreshToken);
 
    // Step 3: Ensure user still exists
 
    const userInDb = await User.findOne({
      _id: decodedRefresh.userId
    });
 
    if (!userInDb) {
      return next(ApiError.unauthorized("Unauthorized, Please login first."));
    }
 
    // Step 4: Issue new tokens
 
    const newAccessToken = generateAccessToken({
      _id: userInDb._id.toString()
    });
 
    const newRefreshToken = generateRefreshToken(decodedRefresh.userId);
 
    // Step 5: Saved accessToken and refreshToken in cookie
    res.cookie("accessToken", newAccessToken, {
      ...COOKIE_OPTIONS,
      maxAge: ACCESS_TOKEN_EXPIRY
    });
 
    res.cookie("refreshToken", newRefreshToken, {
      ...COOKIE_OPTIONS,
      maxAge: REFRESH_TOKEN_EXPIRY
    });
 
    // Step 6: Attach user to request
 
    req.user = {
      _id: decodedRefresh.userId
    };
 
    // you can update the refresh token in the database here if you store it in the database
 
    return next();
  } catch (err: any) {
    logger.warn("Refresh token verification failed");
    return next(ApiError.unauthorized("Unauthorized, Please login first."));
  }
}

Usage Example

src/routes/user.routes.ts
import { Router } from "express";
import { verifyAuthentication } from "../middlewares/verify-auth";
 
const router = Router();
 
router.get("/profile", verifyAuthentication, (req, res) => {
  return ApiResponse.ok(res, "User profile", req.user);
});
 
export default router;

Error Responses

Authentication failures return standardized responses:

{
  "success": false,
  "message": "Unauthorized, Please login first.",
  "statusCode": 401
}

File & Folder Structure

Loading files...

Installation

npx servercn-cli add verify-auth-middleware