refactor(token): streamline token management by implementing secret key caching and enhancing token generation methods

This commit is contained in:
NicolasCantu 2025-11-28 00:13:30 +01:00
parent e1d220596e
commit bbbca27009
2 changed files with 85 additions and 37 deletions

View File

@ -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_BASEURL="your_base_url"
VITE_BOOTSTRAPURL="your_bootstrap_url" VITE_BOOTSTRAPURL="your_bootstrap_url"
VITE_STORAGEURL="your_storage_url" VITE_STORAGEURL="your_storage_url"

View File

@ -1,4 +1,4 @@
import * as jose from 'jose'; import * as jose from "jose";
interface TokenPair { interface TokenPair {
accessToken: string; accessToken: string;
@ -7,10 +7,14 @@ interface TokenPair {
export default class TokenService { export default class TokenService {
private static instance: TokenService; private static instance: TokenService;
private readonly SECRET_KEY = import.meta.env.VITE_JWT_SECRET_KEY;
private readonly ACCESS_TOKEN_EXPIRATION = '30s'; // Constantes
private readonly REFRESH_TOKEN_EXPIRATION = '7d'; private readonly STORAGE_KEY = "4NK_SECURE_SESSION_KEY";
private readonly encoder = new TextEncoder(); 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() {} private constructor() {}
@ -21,17 +25,47 @@ export default class TokenService {
return TokenService.instance; return TokenService.instance;
} }
async generateSessionToken(origin: string): Promise<TokenPair> { /**
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); * 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 accessToken = await new jose.SignJWT({ origin, type: 'access' }) const storedKey = localStorage.getItem(this.STORAGE_KEY);
.setProtectedHeader({ alg: 'HS256' })
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<TokenPair> {
const secret = this.getSecretKey();
const accessToken = await new jose.SignJWT({ origin, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
const refreshToken = await new jose.SignJWT({ origin, type: 'refresh' }) const refreshToken = await new jose.SignJWT({ origin, type: "refresh" })
.setProtectedHeader({ alg: 'HS256' }) .setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.REFRESH_TOKEN_EXPIRATION) .setExpirationTime(this.REFRESH_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
@ -41,47 +75,64 @@ export default class TokenService {
async validateToken(token: string, origin: string): Promise<boolean> { async validateToken(token: string, origin: string): Promise<boolean> {
try { try {
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY)); const secret = this.getSecretKey();
const { payload } = await jose.jwtVerify(token, secret); const { payload } = await jose.jwtVerify(token, secret);
return payload.origin === origin; return payload.origin === origin;
} catch (error: any) { } catch (error: any) {
if (error?.code === 'ERR_JWT_EXPIRED') { // On ignore les erreurs d'expiration classiques pour ne pas polluer la console
console.log('Token expiré'); if (error?.code === "ERR_JWT_EXPIRED") {
return false; return false;
} }
console.error('Erreur de validation du token:', error); console.warn(
"[TokenService] Validation échouée:",
error.code || error.message
);
return false; return false;
} }
} }
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> { async refreshAccessToken(
refreshToken: string,
origin: string
): Promise<string | null> {
try { try {
// Vérifier si le refresh token est valide // Validation du token (vérifie signature + expiration)
const isValid = await this.validateToken(refreshToken, origin); const isValid = await this.validateToken(refreshToken, origin);
if (!isValid) { if (!isValid) return null;
return null;
}
// Vérifier le type du token const secret = this.getSecretKey();
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
const { payload } = await jose.jwtVerify(refreshToken, secret); const { payload } = await jose.jwtVerify(refreshToken, secret);
if (payload.type !== 'refresh') {
return null;
}
// Générer un nouveau access token if (payload.type !== "refresh") return null;
const newAccessToken = await new jose.SignJWT({ origin, type: 'access' })
.setProtectedHeader({ alg: 'HS256' }) // Génération du nouveau token
return await new jose.SignJWT({ origin, type: "access" })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt() .setIssuedAt()
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION) .setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
.sign(secret); .sign(secret);
return newAccessToken;
} catch (error) { } catch (error) {
console.error('Erreur lors du refresh du token:', error); console.error("[TokenService] Erreur refresh:", error);
return null; 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;
}
} }