veza/apps/web/src/services/csrf.ts
2025-12-03 22:56:50 +01:00

133 lines
3.3 KiB
TypeScript

//! Service CSRF pour la protection contre les attaques Cross-Site Request Forgery
//!
//! Ce service gère:
//! - Récupération des tokens CSRF depuis les cookies
//! - Envoi des tokens CSRF dans les requêtes
//! - Gestion des erreurs CSRF
import { ApiError } from '@/types';
export class CsrfService {
private static instance: CsrfService;
private csrfToken: string | null = null;
private tokenExpiry: number | null = null;
private constructor() {
this.loadCsrfToken();
}
public static getInstance(): CsrfService {
if (!CsrfService.instance) {
CsrfService.instance = new CsrfService();
}
return CsrfService.instance;
}
/**
* Charge le token CSRF depuis les cookies
*/
private loadCsrfToken(): void {
try {
// Le token CSRF est stocké dans un cookie httpOnly
// On doit le récupérer via une requête spéciale
this.fetchCsrfToken();
} catch (error) {
console.warn('Erreur lors du chargement du token CSRF:', error);
}
}
/**
* Récupère le token CSRF depuis le serveur
*/
private async fetchCsrfToken(): Promise<void> {
try {
const response = await fetch('/api/v1/csrf-token', {
method: 'GET',
credentials: 'include', // Important pour les cookies
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);
}
const data = await response.json();
this.csrfToken = data.csrf_token;
this.tokenExpiry = data.expires_at
? new Date(data.expires_at).getTime()
: null;
console.debug('Token CSRF chargé:', this.csrfToken);
} catch (error) {
console.error('Erreur lors de la récupération du token CSRF:', error);
throw new ApiError('Impossible de récupérer le token CSRF', 'CSRF_ERROR');
}
}
/**
* Obtient le token CSRF actuel
*/
public getCsrfToken(): string | null {
// Vérifier si le token est expiré
if (this.tokenExpiry && Date.now() > this.tokenExpiry) {
console.warn('Token CSRF expiré, rechargement...');
this.csrfToken = null;
this.tokenExpiry = null;
}
return this.csrfToken;
}
/**
* Force le rechargement du token CSRF
*/
public async refreshCsrfToken(): Promise<void> {
this.csrfToken = null;
this.tokenExpiry = null;
await this.fetchCsrfToken();
}
/**
* Vérifie si un token CSRF est disponible
*/
public hasCsrfToken(): boolean {
return this.getCsrfToken() !== null;
}
/**
* Obtient les headers CSRF pour les requêtes
*/
public getCsrfHeaders(): Record<string, string> {
const token = this.getCsrfToken();
if (!token) {
return {};
}
return {
'X-CSRF-Token': token,
};
}
/**
* Gère les erreurs CSRF
*/
public handleCsrfError(error: any): void {
if (error?.code === 'CSRF_ERROR' || error?.message?.includes('CSRF')) {
console.warn('Erreur CSRF détectée, rechargement du token...');
this.refreshCsrfToken();
}
}
/**
* Nettoie le token CSRF (lors de la déconnexion)
*/
public clearCsrfToken(): void {
this.csrfToken = null;
this.tokenExpiry = null;
}
}
// Instance singleton
export const csrfService = CsrfService.getInstance();