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

This component requires additional ServerCN components.

👉 Note: You do not need to install any servercn dependencies manually. Installing this component will automatically install all required servercn dependencies. Manual installation is optional if you prefer to manage dependencies yourself.

  • HTTP Status Codes:
npx servercn add http-status-codes

Documentation: HTTP Status Codes

  • Api Response Handler:
npx servercn add response-formatter

Documentation: Api Response Handler

  • Api Error Handler:
npx servercn add error-handler

Documentation: Api Error Handler

  • JWT Authentication:
npx servercn add jwt-utils

Documentation: JWT Authentication

  • Logger:
npx servercn add logger

Documentation: Logger

npx servercn add verify-auth-middleware

⚠️ If this dependency is not installed, the component will not function correctly.

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:

src/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.

src/models/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");
    return next(ApiError.badRequest("Invalid or expired access token"));
  }
 
  // 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");
    return next(ApiError.badRequest("Invalid or expired access token"));
  }
 
  // 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

Select a file to view its contents

Installation

npx servercn add verify-auth-middleware