// src/middleware/auth.middleware.js const jwt = require("jsonwebtoken"); const { promisify } = require("util"); const config = require("../config/config"); const { User, Role, UserRole } = require("../models"); const { AuthenticationError, AuthorizationError } = require("../utils/errors"); const logger = require("../utils/logger"); const redis = require("../config/redis"); /** * 토큰 검증 및 사용자 인증 미들웨어 */ const authMiddleware = async (req, res, next) => { try { // 토큰 추출 const token = extractTokenFromHeader(req); if (!token) { throw new AuthenticationError("Authentication token is required"); } // 토큰 검증 const decoded = await verifyToken(token); // 세션 확인 await validateSession(decoded.id, token); // 사용자 정보 조회 및 검증 const user = await getUserWithRoles(decoded.id); validateUser(user); // 마지막 활동 시간 업데이트 await updateLastActivity(user.id); // 요청 객체에 사용자 정보 추가 req.user = user; next(); } catch (error) { handleAuthError(error, res); } }; /** * 특정 권한 필요한 엔드포인트용 미들웨어 */ const requireRole = (...roles) => { return (req, res, next) => { if (!req.user) { return res.status(401).json({ message: "Authentication required" }); } const hasRequiredRole = req.user.Roles.some( (role) => roles.includes(role.name) && role.isActive ); if (!hasRequiredRole) { return res.status(403).json({ message: `Required role: ${roles.join(" or ")}`, }); } next(); }; }; /** * 세부 권한 체크 미들웨어 */ const checkPermission = (resource, action) => { return async (req, res, next) => { try { if (!req.user) { throw new AuthenticationError("Authentication required"); } const hasPermission = await validatePermission( req.user.id, resource, action ); if (!hasPermission) { throw new AuthorizationError( `Permission denied: ${resource}:${action}` ); } next(); } catch (error) { handleAuthError(error, res); } }; }; // 유틸리티 함수들 const extractTokenFromHeader = (req) => { const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { return null; } return authHeader.split(" ")[1]; }; const verifyToken = async (token) => { try { // 토큰 블랙리스트 체크 const isBlacklisted = await redis.get(`blacklist:${token}`); if (isBlacklisted) { throw new AuthenticationError("Token has been revoked"); } return await promisify(jwt.verify)(token, config.jwt.secret); } catch (error) { if (error.name === "TokenExpiredError") { throw new AuthenticationError("Token has expired"); } throw new AuthenticationError("Invalid token"); } }; const validateSession = async (userId, token) => { const currentSession = await redis.get(`session:${userId}`); if (currentSession && currentSession !== token) { throw new AuthenticationError("Session has been invalidated"); } }; const getUserWithRoles = async (userId) => { return await User.findByPk(userId, { attributes: { exclude: ["password"] }, include: [ { model: Role, through: UserRole, attributes: ["id", "name", "permissions", "isActive"], where: { isActive: true }, }, ], }); }; const validateUser = (user) => { if (!user) { throw new AuthenticationError("User not found"); } if (!user.isActive) { throw new AuthenticationError("User account is inactive"); } if (user.Roles.length === 0) { throw new AuthorizationError("User has no active roles"); } }; const validatePermission = async (userId, resource, action) => { // Redis에서 캐시된 권한 확인 const cacheKey = `permissions:${userId}`; let permissions = await redis.get(cacheKey); if (!permissions) { // DB에서 권한 조회 및 캐싱 const user = await User.findByPk(userId, { include: [ { model: Role, attributes: ["permissions"], where: { isActive: true }, }, ], }); permissions = user.Roles.reduce((acc, role) => { const rolePerms = role.permissions || {}; Object.keys(rolePerms).forEach((res) => { acc[res] = acc[res] || []; acc[res].push(...rolePerms[res]); }); return acc; }, {}); await redis.set(cacheKey, JSON.stringify(permissions), "EX", 3600); // 1시간 캐시 } else { permissions = JSON.parse(permissions); } return permissions[resource]?.includes(action) || false; }; const updateLastActivity = async (userId) => { await redis.set(`lastActivity:${userId}`, Date.now()); }; const handleAuthError = (error, res) => { logger.error("Authentication error", { error }); if (error instanceof AuthenticationError) { return res.status(401).json({ message: error.message }); } if (error instanceof AuthorizationError) { return res.status(403).json({ message: error.message }); } return res.status(500).json({ message: "Internal server error" }); }; module.exports = { authMiddleware, requireRole, checkPermission, validatePermission, // Export for testing };