Files
stream-control/packages/server/src/auth.ts
jeanotx32 6f11b72cbb FP
2026-08-11 00:26:56 +02:00

64 lines
2.2 KiB
TypeScript

import { createHmac, createHash, randomBytes, timingSafeEqual } from 'node:crypto';
import type { NextFunction, Request, Response } from 'express';
import { config } from './config.ts';
/** Comparaison à temps constant, tolérante aux longueurs différentes. */
export function safeEqual(a: string, b: string): boolean {
const bufA = Buffer.from(a, 'utf8');
const bufB = Buffer.from(b, 'utf8');
if (bufA.length !== bufB.length) {
// On compare quand même pour ne pas court-circuiter sur la longueur.
timingSafeEqual(bufA, bufA);
return false;
}
return timingSafeEqual(bufA, bufB);
}
export function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
export function generateToken(): string {
return randomBytes(32).toString('base64url');
}
// --- Sessions du dashboard : jeton `<expiration>.<hmac>` sans stockage serveur ---
function sign(payload: string): string {
return createHmac('sha256', config.sessionSecret).update(payload).digest('base64url');
}
export function issueSession(): { token: string; expiresAt: number } {
const expiresAt = Date.now() + config.sessionTtlMs;
const payload = String(expiresAt);
return { token: `${payload}.${sign(payload)}`, expiresAt };
}
export function verifySession(token: string | undefined | null): boolean {
if (!token) return false;
const dot = token.lastIndexOf('.');
if (dot <= 0) return false;
const payload = token.slice(0, dot);
const signature = token.slice(dot + 1);
if (!safeEqual(signature, sign(payload))) return false;
const expiresAt = Number.parseInt(payload, 10);
return Number.isFinite(expiresAt) && expiresAt > Date.now();
}
export function extractBearer(header: string | undefined): string | null {
if (!header) return null;
const [scheme, value] = header.split(' ');
if (!value || scheme?.toLowerCase() !== 'bearer') return null;
return value.trim();
}
/** Middleware Express protégeant les routes /api (hors login). */
export function requireSession(req: Request, res: Response, next: NextFunction): void {
const token = extractBearer(req.headers.authorization) ?? (req.query.token as string | undefined);
if (!verifySession(token)) {
res.status(401).json({ error: 'Non authentifié' });
return;
}
next();
}