From 5729967c9fea5f5be7e668a343d0b089aae7d794 Mon Sep 17 00:00:00 2001 From: riya-chauhan12 Date: Wed, 17 Jun 2026 19:41:47 +0530 Subject: [PATCH 1/4] fix: validate refresh token hash during silent refresh --- server/middlewares/authMiddleware.js | 54 ++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/server/middlewares/authMiddleware.js b/server/middlewares/authMiddleware.js index de23ddd..a0e01cb 100644 --- a/server/middlewares/authMiddleware.js +++ b/server/middlewares/authMiddleware.js @@ -1,6 +1,7 @@ import { verifyToken, verifyRefreshToken, generateAccessToken, setAccessTokenCookie } from "../utils/tokenHelper.js"; import User from "../models/User.js"; import ApiError from "../utils/ApiError.js"; +import bcrypt from "bcryptjs"; /** * Auth Middleware @@ -44,19 +45,50 @@ const authMiddleware = async (req, res, next) => { try { const refreshDecoded = verifyRefreshToken(req.cookies.refreshToken); const userId = refreshDecoded.userId || refreshDecoded.id || refreshDecoded._id; - const newAccessToken = generateAccessToken({ - userId, - email: refreshDecoded.email, - role: refreshDecoded.role + // Validate refresh token against stored hash + const refreshUser = await User.findById(userId).select("+security.refreshTokenHash"); + if (!refreshUser) { + throw new ApiError(401, "User not found."); + } + const storedHash = refreshUser.security?.refreshTokenHash; + if (!storedHash) { + throw new ApiError( + 401, + "Session has been revoked. Please log in again." + ); + } + const isValid = await bcrypt.compare( + req.cookies.refreshToken, + storedHash + ); + + if (!isValid) { + throw new ApiError( + 401, + "Invalid session. Please log in again." + ); + } + const newAccessToken = generateAccessToken({userId: refreshUser._id, email: refreshUser.email, role: refreshUser.role }); - // Set only the new access token cookie — refresh token stays unchanged. - // Cookie options are centralised in setAccessTokenCookie (tokenHelper.js). + + // Set only the new access token cookie setAccessTokenCookie(res, newAccessToken); - decoded = { userId, email: refreshDecoded.email, role: refreshDecoded.role }; - } catch { - throw new ApiError(401, "Session expired. Please log in again."); - } - } else { + + decoded = {userId: refreshUser._id ,email: refreshUser.email, role: refreshUser.role + }; + + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + + throw new ApiError( + 401, + "Session expired. Please log in again." + ); + + } + }else { throw new ApiError(401, "Invalid or expired token."); } } From ee1fdb1ade9ce4da25ee657e17a4ef4de3839c44 Mon Sep 17 00:00:00 2001 From: riya-chauhan12 Date: Thu, 18 Jun 2026 13:09:18 +0530 Subject: [PATCH 2/4] fix: address review feedback for silent refresh revocation --- server/middlewares/authMiddleware.js | 90 ++++++++++++++++------------ 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/server/middlewares/authMiddleware.js b/server/middlewares/authMiddleware.js index a0e01cb..8b2dd36 100644 --- a/server/middlewares/authMiddleware.js +++ b/server/middlewares/authMiddleware.js @@ -1,4 +1,4 @@ -import { verifyToken, verifyRefreshToken, generateAccessToken, setAccessTokenCookie } from "../utils/tokenHelper.js"; +import { verifyToken, verifyRefreshToken, generateAccessToken, setAccessTokenCookie, clearAuthCookies } from "../utils/tokenHelper.js"; import User from "../models/User.js"; import ApiError from "../utils/ApiError.js"; import bcrypt from "bcryptjs"; @@ -36,6 +36,8 @@ const authMiddleware = async (req, res, next) => { throw new ApiError(401, "Access denied. No token provided."); } + let prefetchedUser = null; + let decoded; try { decoded = verifyToken(token); @@ -47,79 +49,93 @@ const authMiddleware = async (req, res, next) => { const userId = refreshDecoded.userId || refreshDecoded.id || refreshDecoded._id; // Validate refresh token against stored hash const refreshUser = await User.findById(userId).select("+security.refreshTokenHash"); + if (!refreshUser) { throw new ApiError(401, "User not found."); } + const storedHash = refreshUser.security?.refreshTokenHash; if (!storedHash) { - throw new ApiError( - 401, - "Session has been revoked. Please log in again." - ); + clearAuthCookies(res); + throw new ApiError(401, "Session has been revoked. Please log in again."); } + const isValid = await bcrypt.compare( req.cookies.refreshToken, storedHash - ); - + ); + if (!isValid) { + await User.findByIdAndUpdate(userId, { + $unset: { "security.refreshTokenHash": "" } + }); + + clearAuthCookies(res); + throw new ApiError( 401, "Invalid session. Please log in again." ); } - const newAccessToken = generateAccessToken({userId: refreshUser._id, email: refreshUser.email, role: refreshUser.role + const newAccessToken = generateAccessToken({ + userId: refreshUser._id, + email: refreshUser.email, + role: refreshUser.role }); // Set only the new access token cookie setAccessTokenCookie(res, newAccessToken); - decoded = {userId: refreshUser._id ,email: refreshUser.email, role: refreshUser.role - }; + decoded = { + userId: refreshUser._id, + email: refreshUser.email, + role: refreshUser.role + }; + prefetchedUser = refreshUser; + } catch (error) { if (error instanceof ApiError) { - throw error; + throw error; } throw new ApiError( 401, "Session expired. Please log in again." ); - - } - }else { - throw new ApiError(401, "Invalid or expired token."); + } + } else { + throw new ApiError(401, "Invalid or expired token."); + } } - } - const userId = decoded.userId || decoded.id || decoded._id; + const userId = decoded.userId || decoded.id || decoded._id; - if (!userId) { - throw new ApiError(401, "Invalid token payload."); - } + if (!userId) { + throw new ApiError(401, "Invalid token payload."); + } - const user = await User.findById(userId).select("-password"); + const user = prefetchedUser ?? await User.findById(userId).select("-password"); + if (!user) { + throw new ApiError(401, "User not found or account deleted."); + } - if (!user) { - throw new ApiError(401, "User not found or account deleted."); - } + req.user = user; + next(); + } catch (error) { + if (error instanceof ApiError) { + return res.status(error.statusCode).json({ + success: false, + message: error.message + }); + } - req.user = user; - next(); - } catch (error) { - if (error instanceof ApiError) { - return res.status(error.statusCode).json({ + return res.status(401).json({ success: false, - message: error.message + message: "Invalid or expired token." }); + } - - return res.status(401).json({ - success: false, - message: "Invalid or expired token." - }); - } -}; + }; export default authMiddleware; From 4ecdad5362ec72a31b71da6ee10dda40b78e8049 Mon Sep 17 00:00:00 2001 From: riya-chauhan12 Date: Thu, 18 Jun 2026 13:28:22 +0530 Subject: [PATCH 3/4] fix: address review feedback for silent refresh revocation --- server/middlewares/authMiddleware.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/server/middlewares/authMiddleware.js b/server/middlewares/authMiddleware.js index 8b2dd36..85a6820 100644 --- a/server/middlewares/authMiddleware.js +++ b/server/middlewares/authMiddleware.js @@ -14,6 +14,7 @@ import bcrypt from "bcryptjs"; * we transparently issue a new access token cookie (silent refresh). */ const authMiddleware = async (req, res, next) => { + try { let token = null; let tokenSource = null; @@ -47,6 +48,7 @@ const authMiddleware = async (req, res, next) => { try { const refreshDecoded = verifyRefreshToken(req.cookies.refreshToken); const userId = refreshDecoded.userId || refreshDecoded.id || refreshDecoded._id; + // Validate refresh token against stored hash const refreshUser = await User.findById(userId).select("+security.refreshTokenHash"); @@ -73,8 +75,8 @@ const authMiddleware = async (req, res, next) => { clearAuthCookies(res); throw new ApiError( - 401, - "Invalid session. Please log in again." + 401, + "Invalid session. Please log in again." ); } const newAccessToken = generateAccessToken({ @@ -90,7 +92,7 @@ const authMiddleware = async (req, res, next) => { userId: refreshUser._id, email: refreshUser.email, role: refreshUser.role - }; + }; prefetchedUser = refreshUser; @@ -105,9 +107,9 @@ const authMiddleware = async (req, res, next) => { ); } } else { - throw new ApiError(401, "Invalid or expired token."); - } + throw new ApiError(401, "Invalid or expired token."); } + } const userId = decoded.userId || decoded.id || decoded._id; @@ -133,8 +135,7 @@ const authMiddleware = async (req, res, next) => { return res.status(401).json({ success: false, message: "Invalid or expired token." - }); - + }); } }; From c9dd2c1db05ffdf146a52781ec19534642ad4cc4 Mon Sep 17 00:00:00 2001 From: riya-chauhan12 Date: Thu, 18 Jun 2026 13:47:40 +0530 Subject: [PATCH 4/4] fix: remove refresh token hash from prefetched user --- server/middlewares/authMiddleware.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/middlewares/authMiddleware.js b/server/middlewares/authMiddleware.js index 85a6820..162cad6 100644 --- a/server/middlewares/authMiddleware.js +++ b/server/middlewares/authMiddleware.js @@ -94,6 +94,9 @@ const authMiddleware = async (req, res, next) => { role: refreshUser.role }; + if (refreshUser.security) { + refreshUser.security.refreshTokenHash = undefined; + } prefetchedUser = refreshUser; } catch (error) {