splitwise / src /lib /server /auth /session.ts
assafvayner's picture
assafvayner HF Staff
fix(auth): bound scrypt params, cap concurrency, evict rate-limit keys, re-bootstrap admin
1c7b998
Raw
History Blame Contribute Delete
1.79 kB
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;
}
}