FP
This commit is contained in:
63
packages/server/src/auth.ts
Normal file
63
packages/server/src/auth.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user