From 6c266cbd70ebe3514aa7a75599de0ff3f86ecdc8 Mon Sep 17 00:00:00 2001 From: Atibali Date: Sat, 28 Feb 2026 09:59:20 +0530 Subject: [PATCH] fix(security): invalidate old JWTs after password change --- src/middlewares/auth.middleware.js | 27 +++++++++++++++++++++++---- src/services/auth.service.js | 18 ++++++++++++++---- src/utils/jwt.js | 16 ++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/middlewares/auth.middleware.js b/src/middlewares/auth.middleware.js index e49a5c9..dfdca55 100644 --- a/src/middlewares/auth.middleware.js +++ b/src/middlewares/auth.middleware.js @@ -1,8 +1,16 @@ -const { verifyToken } = require("../utils/jwt"); +const { verifyToken, createPasswordVersion } = require("../utils/jwt"); const { prisma } = require("../config/prisma"); const logger = require("../utils/logger"); const authService = require("../services/auth.service"); +const isPasswordVersionValid = (decoded, passwordHash) => { + if (!decoded?.pwdv || !passwordHash) { + return false; + } + + return decoded.pwdv === createPasswordVersion(passwordHash); +}; + /** * Authentication middleware * Verifies JWT token and attaches user to request object @@ -52,6 +60,7 @@ const authenticate = async (req, res, next) => { email: true, username: true, leetcodeUsername: true, + password: true, createdAt: true, }, }); @@ -63,8 +72,16 @@ const authenticate = async (req, res, next) => { }); } + if (!isPasswordVersionValid(decoded, user.password)) { + return res.status(401).json({ + success: false, + message: "Token is no longer valid. Please log in again.", + }); + } + // Attach user to request object - req.user = user; + const { password, ...safeUser } = user; + req.user = safeUser; next(); } catch (error) { @@ -96,12 +113,14 @@ const optionalAuthenticate = async (req, res, next) => { email: true, username: true, leetcodeUsername: true, + password: true, createdAt: true, }, }); - if (user) { - req.user = user; + if (user && isPasswordVersionValid(decoded, user.password)) { + const { password, ...safeUser } = user; + req.user = safeUser; } } catch (error) { // Token invalid, but we don't return error for optional auth diff --git a/src/services/auth.service.js b/src/services/auth.service.js index ee2b7f2..8957b1f 100644 --- a/src/services/auth.service.js +++ b/src/services/auth.service.js @@ -2,7 +2,11 @@ const bcrypt = require("bcryptjs"); const crypto = require("crypto"); const { prisma } = require("../config/prisma"); const { config } = require("../config/env"); -const { generateToken, decodeToken } = require("../utils/jwt"); +const { + generateToken, + decodeToken, + createPasswordVersion, +} = require("../utils/jwt"); const { AppError } = require("../middlewares/error.middleware"); const logger = require("../utils/logger"); const { logAudit } = require("../utils/auditLogger"); @@ -44,7 +48,10 @@ const register = async (userData) => { }); // Generate JWT token - const token = generateToken({ userId: user.id }); + const token = generateToken({ + userId: user.id, + pwdv: createPasswordVersion(hashedPassword), + }); logger.info(`New user registered: ${user.username} (${user.email})`); @@ -91,8 +98,11 @@ const login = async (emailOrUsername, password) => { throw new AppError("Invalid credentials", 401); } - // Generate token - const token = generateToken({ userId: user.id }); + // Generate JWT token + const token = generateToken({ + userId: user.id, + pwdv: createPasswordVersion(user.password), + }); logger.info(`User logged in: ${user.username}`); diff --git a/src/utils/jwt.js b/src/utils/jwt.js index 8e4d4a1..17253b7 100644 --- a/src/utils/jwt.js +++ b/src/utils/jwt.js @@ -1,4 +1,5 @@ const jwt = require("jsonwebtoken"); +const crypto = require("crypto"); const { config } = require("../config/env"); /** @@ -41,8 +42,23 @@ const decodeToken = (token) => { return jwt.decode(token); }; +/** + * Derive a stable token-bound password version from the stored hash. + * This changes whenever password hash changes, invalidating old tokens. + * @param {string} passwordHash - Bcrypt password hash from database + * @returns {string} Password version fingerprint + */ +const createPasswordVersion = (passwordHash) => { + return crypto + .createHmac("sha256", config.jwtSecret) + .update(passwordHash) + .digest("hex") + .slice(0, 24); +}; + module.exports = { generateToken, verifyToken, decodeToken, + createPasswordVersion, };