133 lines
3.3 KiB
TypeScript
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();
|