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 9085617..8ae7f44 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"); @@ -50,6 +54,12 @@ const register = async (userData) => { }, }); + // Generate JWT token + const token = generateToken({ + userId: user.id, + pwdv: createPasswordVersion(hashedPassword), + }); + logger.info(`New user registered: ${user.username} (${user.email})`); sendVerificationEmail( @@ -120,15 +130,11 @@ const login = async (emailOrUsername, password) => { throw new AppError("Invalid credentials", 401); } - if (!user.isEmailVerified) { - throw new AppError( - "Please verify your email before logging in.", - 403 - ); -} - // Generate JWT token - const token = generateToken({ userId: user.id }); + 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, };