Spaces:
Sleeping
Sleeping
File size: 1,793 Bytes
ef84ace 1c7b998 ef84ace | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | import { createHmac, timingSafeEqual } from 'node:crypto';
export interface SessionPayload {
uid: string;
sv: number;
/** Expiry, ms since epoch. */
exp: number;
}
export const SESSION_COOKIE = 'session';
export const SESSION_TTL_SECONDS = 30 * 24 * 60 * 60;
function sign(data: string, secret: string): string {
return createHmac('sha256', secret).update(data).digest('base64url');
}
export function createSessionToken(payload: Omit<SessionPayload, 'exp'>, secret: string, now = new Date(), ttlSeconds = SESSION_TTL_SECONDS): string {
const full: SessionPayload = { ...payload, exp: now.getTime() + ttlSeconds * 1000 };
const data = Buffer.from(JSON.stringify(full)).toString('base64url');
return `${data}.${sign(data, secret)}`;
}
/**
* Verifies only the token's signature and expiry. This does NOT prove the session is still
* valid: callers MUST also check that `payload.uid` refers to a user that still exists, is
* active, and that `user.sessionVersion === payload.sv` (the hooks do this).
*/
export function verifySessionToken(token: string, secret: string, now = new Date()): SessionPayload | null {
const dot = token.lastIndexOf('.');
if (dot <= 0) return null;
const data = token.slice(0, dot);
const sig = token.slice(dot + 1);
const expected = sign(data, secret);
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
try {
const parsed = JSON.parse(Buffer.from(data, 'base64url').toString()) as Partial<SessionPayload>;
if (typeof parsed.uid !== 'string' || typeof parsed.sv !== 'number' || typeof parsed.exp !== 'number') return null;
if (parsed.exp <= now.getTime()) return null;
return { uid: parsed.uid, sv: parsed.sv, exp: parsed.exp };
} catch {
return null;
}
}
|