64 lines
2.2 KiB
TypeScript
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();
|
|
}
|