File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
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 };
}