refactor(token): streamline token management by implementing secret key caching and enhancing token generation methods
This commit is contained in:
parent
e1d220596e
commit
bbbca27009
@ -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"
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<TokenPair> {
|
async generateSessionToken(origin: string): Promise<TokenPair> {
|
||||||
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
const secret = this.getSecretKey();
|
||||||
|
|
||||||
const accessToken = await new jose.SignJWT({ origin, type: 'access' })
|
const accessToken = await new jose.SignJWT({ origin, type: "access" })
|
||||||
.setProtectedHeader({ alg: 'HS256' })
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user