import 'server-only'; import { createHmac, timingSafeEqual } from 'node:crypto'; import { getSessionSecret } from '@/lib/env'; /** * 세션 토큰 발급·검증 — Node 내장 crypto로 서명하는 stateless HMAC-SHA256 토큰. * 형식: `base64url(JSON payload).base64url(signature)`. * 서명 불일치·만료·파싱 실패 등 어떤 이유로든 검증에 실패하면 null을 반환한다(fail-closed). */ const TOKEN_TTL_SECONDS = 60 * 60 * 12; // 12시간 — 보안 우선으로 짧게 설정 type SessionTokenPayload = { adminId: string; iat: number; exp: number; }; function base64UrlEncode(input: string): string { return Buffer.from(input, 'utf8').toString('base64url'); } function base64UrlDecode(input: string): string { return Buffer.from(input, 'base64url').toString('utf8'); } function sign(encodedPayload: string): string { return createHmac('sha256', getSessionSecret()) .update(encodedPayload) .digest('base64url'); } export function createSessionToken(adminId: string): string { const issuedAt = Math.floor(Date.now() / 1000); const payload: SessionTokenPayload = { adminId, iat: issuedAt, exp: issuedAt + TOKEN_TTL_SECONDS, }; const encodedPayload = base64UrlEncode(JSON.stringify(payload)); const signature = sign(encodedPayload); return `${encodedPayload}.${signature}`; } export function verifySessionToken(token: string): { adminId: string } | null { const [encodedPayload, signature] = token.split('.'); if (!encodedPayload || !signature) { return null; } const expectedSignature = sign(encodedPayload); const signatureBuffer = Buffer.from(signature); const expectedBuffer = Buffer.from(expectedSignature); if ( signatureBuffer.length !== expectedBuffer.length || !timingSafeEqual(signatureBuffer, expectedBuffer) ) { return null; } let payload: SessionTokenPayload; try { payload = JSON.parse(base64UrlDecode(encodedPayload)) as SessionTokenPayload; } catch { return null; } if (typeof payload.adminId !== 'string' || !payload.adminId) { return null; } const now = Math.floor(Date.now() / 1000); if (typeof payload.exp !== 'number' || payload.exp <= now) { return null; } return { adminId: payload.adminId }; }