- Added comprehensive validation for user data during registration and updates, including email format, company code existence, user type validation, and password length checks. - Implemented JWT token invalidation for users when their status changes or when roles are updated, ensuring security and compliance with the latest policies. - Introduced a new TokenInvalidationService to manage token versioning and invalidation processes efficiently. - Updated the admin controller to provide detailed error messages and success responses for user status changes and validations. - Enhanced the authentication middleware to check token versions against the database, ensuring that invalidated tokens cannot be used. This commit improves the overall security and user management experience within the application.
289 lines
7.6 KiB
TypeScript
289 lines
7.6 KiB
TypeScript
// 인증 미들웨어
|
|
// JWT 토큰 검증 및 사용자 정보 설정
|
|
|
|
import { Request, Response, NextFunction } from "express";
|
|
import { JwtUtils } from "../utils/jwtUtils";
|
|
import { AuthenticatedRequest, PersonBean } from "../types/auth";
|
|
import { logger } from "../utils/logger";
|
|
import { TokenInvalidationService } from "../services/tokenInvalidationService";
|
|
|
|
// AuthenticatedRequest 타입을 다른 모듈에서 사용할 수 있도록 re-export
|
|
export { AuthenticatedRequest } from "../types/auth";
|
|
|
|
// Express Request 타입 확장
|
|
declare global {
|
|
namespace Express {
|
|
interface Request {
|
|
ip: string;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* JWT 토큰 검증 미들웨어
|
|
* 기존 세션 방식과 동일한 효과를 제공
|
|
*/
|
|
export const authenticateToken = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): Promise<void> => {
|
|
try {
|
|
// Authorization 헤더에서 토큰 추출
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1]; // Bearer TOKEN
|
|
|
|
if (!token) {
|
|
res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: "TOKEN_MISSING",
|
|
details: "인증 토큰이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// JWT 토큰 검증 및 사용자 정보 추출
|
|
const userInfo: PersonBean = JwtUtils.verifyToken(token);
|
|
|
|
// token_version 검증 (JWT payload vs DB)
|
|
const decoded = JwtUtils.decodeToken(token);
|
|
const tokenVersion = decoded?.tokenVersion;
|
|
|
|
// tokenVersion이 undefined면 구버전 토큰이므로 통과 (하위 호환)
|
|
if (tokenVersion !== undefined) {
|
|
const dbVersion = await TokenInvalidationService.getUserTokenVersion(userInfo.userId);
|
|
if (tokenVersion !== dbVersion) {
|
|
res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: "TOKEN_INVALIDATED",
|
|
details: "보안 정책에 의해 재로그인이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 요청 객체에 사용자 정보 설정 (기존 PersonBean과 동일)
|
|
req.user = userInfo;
|
|
|
|
// 로그 기록
|
|
logger.info(`인증 성공: ${userInfo.userId} (${req.ip})`);
|
|
|
|
next();
|
|
} catch (error) {
|
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
logger.error(`인증 실패: ${errorMessage} (${req.ip})`);
|
|
|
|
// 토큰 만료 에러인지 확인
|
|
const isTokenExpired = errorMessage.includes("만료");
|
|
|
|
res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: isTokenExpired ? "TOKEN_EXPIRED" : "INVALID_TOKEN",
|
|
details: errorMessage || "토큰 검증에 실패했습니다.",
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 선택적 인증 미들웨어 (토큰이 없어도 통과)
|
|
* 일부 API에서 사용 (예: 공개 정보 조회)
|
|
*/
|
|
export const optionalAuth = (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void => {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (token) {
|
|
const userInfo: PersonBean = JwtUtils.verifyToken(token);
|
|
req.user = userInfo;
|
|
logger.debug(`선택적 인증 성공: ${userInfo.userId} (${req.ip})`);
|
|
} else {
|
|
logger.debug(`선택적 인증: 토큰 없음 (${req.ip})`);
|
|
}
|
|
|
|
next();
|
|
} catch (error) {
|
|
// 토큰이 있지만 유효하지 않은 경우에도 통과 (선택적 인증)
|
|
logger.warn(
|
|
`선택적 인증 실패: ${error instanceof Error ? error.message : "Unknown error"} (${req.ip})`
|
|
);
|
|
next();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 관리자 권한 확인 미들웨어
|
|
*/
|
|
export const requireAdmin = (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void => {
|
|
if (!req.user) {
|
|
res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: "AUTHENTICATION_REQUIRED",
|
|
details: "인증이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 기존 Java 로직과 동일: plm_admin 사용자만 관리자로 인식
|
|
if (req.user.userId === "plm_admin") {
|
|
next();
|
|
} else {
|
|
res.status(403).json({
|
|
success: false,
|
|
error: {
|
|
code: "ADMIN_REQUIRED",
|
|
details: "관리자 권한이 필요합니다.",
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 특정 사용자 또는 관리자 권한 확인 미들웨어
|
|
*/
|
|
export const requireUserOrAdmin = (targetUserId: string) => {
|
|
return (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void => {
|
|
if (!req.user) {
|
|
res.status(401).json({
|
|
success: false,
|
|
error: {
|
|
code: "AUTHENTICATION_REQUIRED",
|
|
details: "인증이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 본인 또는 관리자인 경우 통과
|
|
if (req.user.userId === targetUserId || req.user.userId === "plm_admin") {
|
|
next();
|
|
} else {
|
|
res.status(403).json({
|
|
success: false,
|
|
error: {
|
|
code: "PERMISSION_DENIED",
|
|
details: "권한이 없습니다.",
|
|
},
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* 토큰 갱신 미들웨어
|
|
* 토큰이 곧 만료될 경우 자동으로 갱신
|
|
*/
|
|
export const refreshTokenIfNeeded = async (
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): Promise<void> => {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (token) {
|
|
// 토큰이 1시간 이내에 만료되는지 확인
|
|
const decoded = JwtUtils.decodeToken(token);
|
|
if (decoded && decoded.exp) {
|
|
const currentTime = Math.floor(Date.now() / 1000);
|
|
const timeUntilExpiry = decoded.exp - currentTime;
|
|
|
|
// 1시간(3600초) 이내에 만료되는 경우 갱신
|
|
if (timeUntilExpiry > 0 && timeUntilExpiry < 3600) {
|
|
// 갱신 전 token_version 검증
|
|
if (decoded.tokenVersion !== undefined) {
|
|
const dbVersion = await TokenInvalidationService.getUserTokenVersion(decoded.userId);
|
|
if (decoded.tokenVersion !== dbVersion) {
|
|
// 무효화된 토큰은 갱신하지 않음
|
|
next();
|
|
return;
|
|
}
|
|
}
|
|
|
|
const newToken = JwtUtils.refreshToken(token);
|
|
|
|
// 새로운 토큰을 응답 헤더에 포함
|
|
res.setHeader("X-New-Token", newToken);
|
|
logger.info(`토큰 갱신: ${decoded.userId} (${req.ip})`);
|
|
}
|
|
}
|
|
}
|
|
|
|
next();
|
|
} catch (error) {
|
|
// 토큰 갱신 실패해도 요청은 계속 진행
|
|
logger.warn(
|
|
`토큰 갱신 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
);
|
|
next();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 인증 상태 확인 미들웨어
|
|
* 토큰 유효성만 확인하고 사용자 정보는 설정하지 않음
|
|
*/
|
|
export const checkAuthStatus = (
|
|
req: Request,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void => {
|
|
try {
|
|
const authHeader = req.get("Authorization");
|
|
const token = authHeader && authHeader.split(" ")[1];
|
|
|
|
if (!token) {
|
|
res.status(200).json({
|
|
success: true,
|
|
data: {
|
|
isAuthenticated: false,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
const validation = JwtUtils.validateToken(token);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: {
|
|
isAuthenticated: validation.isValid,
|
|
error: validation.error,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`인증 상태 확인 실패: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: {
|
|
isAuthenticated: false,
|
|
error: "인증 상태 확인 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
};
|