import * as jose from "jose"; interface TokenPair { accessToken: string; refreshToken: string; } export default class TokenService { private static instance: TokenService; // Constantes private readonly STORAGE_KEY = "4NK_SECURE_SESSION_KEY"; private readonly ACCESS_TOKEN_EXPIRATION = "30s"; private readonly REFRESH_TOKEN_EXPIRATION = "7d"; // Cache mémoire pour éviter de lire le localStorage à chaque appel private secretKeyCache: Uint8Array | null = null; private constructor() {} static async getInstance(): Promise { if (!TokenService.instance) { TokenService.instance = new TokenService(); } return TokenService.instance; } /** * Récupère la clé secrète existante ou en génère une nouvelle * et la sauvegarde dans le localStorage pour survivre aux refresh. */ private getSecretKey(): Uint8Array { if (this.secretKeyCache) return this.secretKeyCache; const storedKey = localStorage.getItem(this.STORAGE_KEY); if (storedKey) { // Restauration de la clé existante (Hex -> Uint8Array) this.secretKeyCache = this.hexToBuffer(storedKey); } else { // Génération d'une nouvelle clé aléatoire de 32 octets (256 bits) const newKey = new Uint8Array(32); crypto.getRandomValues(newKey); // Sauvegarde (Uint8Array -> Hex) localStorage.setItem(this.STORAGE_KEY, this.bufferToHex(newKey)); this.secretKeyCache = newKey; console.log( "[TokenService] 🔐 Nouvelle clé de session générée et stockée." ); } return this.secretKeyCache; } // --- Méthodes Publiques --- async generateSessionToken(origin: string): Promise { const secret = this.getSecretKey(); const accessToken = await new jose.SignJWT({ origin, type: "access" }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .sign(secret); const refreshToken = await new jose.SignJWT({ origin, type: "refresh" }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(this.REFRESH_TOKEN_EXPIRATION) .sign(secret); return { accessToken, refreshToken }; } async validateToken(token: string, origin: string): Promise { try { const secret = this.getSecretKey(); const { payload } = await jose.jwtVerify(token, secret); return payload.origin === origin; } catch (error: any) { // On ignore les erreurs d'expiration classiques pour ne pas polluer la console if (error?.code === "ERR_JWT_EXPIRED") { return false; } console.warn( "[TokenService] Validation échouée:", error.code || error.message ); return false; } } async refreshAccessToken( refreshToken: string, origin: string ): Promise { try { // Validation du token (vérifie signature + expiration) const isValid = await this.validateToken(refreshToken, origin); if (!isValid) return null; const secret = this.getSecretKey(); const { payload } = await jose.jwtVerify(refreshToken, secret); if (payload.type !== "refresh") return null; // Génération du nouveau token return await new jose.SignJWT({ origin, type: "access" }) .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .sign(secret); } catch (error) { console.error("[TokenService] Erreur refresh:", error); return null; } } // --- Utilitaires de conversion --- private bufferToHex(buffer: Uint8Array): string { return Array.from(buffer) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } private hexToBuffer(hex: string): Uint8Array { if (hex.length % 2 !== 0) throw new Error("Invalid hex string"); const bytes = new Uint8Array(hex.length / 2); for (let i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } }