diff --git a/.env.exemple b/.env.exemple index 822c500..47e0137 100644 --- a/.env.exemple +++ b/.env.exemple @@ -1,6 +1,3 @@ -VITE_API_URL=https://api.example.com -VITE_API_KEY=your_api_key -VITE_JWT_SECRET_KEY=your_secret_key VITE_BASEURL="your_base_url" VITE_BOOTSTRAPURL="your_bootstrap_url" VITE_STORAGEURL="your_storage_url" diff --git a/src/services/token.service.ts b/src/services/token.service.ts index 6c7a4d9..25f0bf5 100644 --- a/src/services/token.service.ts +++ b/src/services/token.service.ts @@ -1,4 +1,4 @@ -import * as jose from 'jose'; +import * as jose from "jose"; interface TokenPair { accessToken: string; @@ -7,10 +7,14 @@ interface TokenPair { export default class TokenService { private static instance: TokenService; - private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY; - private readonly ACCESS_TOKEN_EXPIRATION = '30s'; - private readonly REFRESH_TOKEN_EXPIRATION = '7d'; - private readonly encoder = new TextEncoder(); + + // 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() {} @@ -21,17 +25,47 @@ export default class 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 = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); - - const accessToken = await new jose.SignJWT({ origin, type: 'access' }) - .setProtectedHeader({ alg: 'HS256' }) + 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' }) + const refreshToken = await new jose.SignJWT({ origin, type: "refresh" }) + .setProtectedHeader({ alg: "HS256" }) .setIssuedAt() .setExpirationTime(this.REFRESH_TOKEN_EXPIRATION) .sign(secret); @@ -41,47 +75,64 @@ export default class TokenService { async validateToken(token: string, origin: string): Promise { try { - const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); + const secret = this.getSecretKey(); const { payload } = await jose.jwtVerify(token, secret); - + return payload.origin === origin; } catch (error: any) { - if (error?.code === 'ERR_JWT_EXPIRED') { - console.log('Token expiré'); + // On ignore les erreurs d'expiration classiques pour ne pas polluer la console + if (error?.code === "ERR_JWT_EXPIRED") { return false; } - - console.error('Erreur de validation du token:', error); + + console.warn( + "[TokenService] Validation échouée:", + error.code || error.message + ); return false; } } - async refreshAccessToken(refreshToken: string, origin: string): Promise { + async refreshAccessToken( + refreshToken: string, + origin: string + ): Promise { try { - // Vérifier si le refresh token est valide + // Validation du token (vérifie signature + expiration) const isValid = await this.validateToken(refreshToken, origin); - if (!isValid) { - return null; - } + if (!isValid) return null; - // Vérifier le type du token - const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); + const secret = this.getSecretKey(); const { payload } = await jose.jwtVerify(refreshToken, secret); - if (payload.type !== 'refresh') { - return null; - } - // Générer un nouveau access token - const newAccessToken = await new jose.SignJWT({ origin, type: 'access' }) - .setProtectedHeader({ alg: 'HS256' }) + 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); - - return newAccessToken; } catch (error) { - console.error('Erreur lors du refresh du token:', error); + console.error("[TokenService] Erreur refresh:", error); return null; } } -} \ No newline at end of file + + // --- 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; + } +}