diff --git a/server/middlewares/authMiddleware.js b/server/middlewares/authMiddleware.js index de23ddd..162cad6 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 { 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"; /** * Auth Middleware @@ -13,6 +14,7 @@ import ApiError from "../utils/ApiError.js"; * we transparently issue a new access token cookie (silent refresh). */ const authMiddleware = async (req, res, next) => { + try { let token = null; let tokenSource = null; @@ -35,6 +37,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); @@ -44,50 +48,98 @@ 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"); + + if (!refreshUser) { + throw new ApiError(401, "User not found."); + } + + const storedHash = refreshUser.security?.refreshTokenHash; + if (!storedHash) { + 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, - email: refreshDecoded.email, - role: refreshDecoded.role + 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."); - } + + decoded = { + userId: refreshUser._id, + email: refreshUser.email, + role: refreshUser.role + }; + + if (refreshUser.security) { + refreshUser.security.refreshTokenHash = undefined; + } + prefetchedUser = refreshUser; + + } 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."); } } - 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;