101 lines
3.1 KiB
TypeScript
101 lines
3.1 KiB
TypeScript
import * as jose from 'jose';
|
|
|
|
interface TokenPair {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
}
|
|
|
|
export default class TokenService {
|
|
private static instance: TokenService;
|
|
private readonly SECRET_KEY = ((): string => {
|
|
// Récupération de la clé depuis l'environnement Vite (via eval) ou fallback Node (tests)
|
|
const viteEnv = (() => {
|
|
try {
|
|
// évite l'erreur de parsing Jest en CJS
|
|
return (0, eval)('import.meta')?.env;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
})() as any | undefined;
|
|
const viteKey = viteEnv?.VITE_JWT_SECRET_KEY as string | undefined;
|
|
const nodeKey = (globalThis as any)?.process?.env?.VITE_JWT_SECRET_KEY as string | undefined;
|
|
const resolved = viteKey || nodeKey || '';
|
|
return resolved;
|
|
})();
|
|
private readonly ACCESS_TOKEN_EXPIRATION = '30s';
|
|
private readonly REFRESH_TOKEN_EXPIRATION = '7d';
|
|
private readonly encoder = new TextEncoder();
|
|
|
|
private constructor() {}
|
|
|
|
static async getInstance(): Promise<TokenService> {
|
|
if (!TokenService.instance) {
|
|
TokenService.instance = new TokenService();
|
|
}
|
|
return TokenService.instance;
|
|
}
|
|
|
|
async generateSessionToken(origin: string): Promise<TokenPair> {
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
|
|
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<boolean> {
|
|
try {
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
const { payload } = await jose.jwtVerify(token, secret);
|
|
|
|
return payload.origin === origin;
|
|
} catch (error: any) {
|
|
if (error?.code === 'ERR_JWT_EXPIRED') {
|
|
console.log('Token expiré');
|
|
return false;
|
|
}
|
|
|
|
console.error('Erreur de validation du token:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async refreshAccessToken(refreshToken: string, origin: string): Promise<string | null> {
|
|
try {
|
|
// Vérifier si le refresh token est valide
|
|
const isValid = await this.validateToken(refreshToken, origin);
|
|
if (!isValid) {
|
|
return null;
|
|
}
|
|
|
|
// Vérifier le type du token
|
|
const secret = new Uint8Array(this.encoder.encode(this.SECRET_KEY));
|
|
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' })
|
|
.setIssuedAt()
|
|
.setExpirationTime(this.ACCESS_TOKEN_EXPIRATION)
|
|
.sign(secret);
|
|
|
|
return newAccessToken;
|
|
} catch (error) {
|
|
console.error('Erreur lors du refresh du token:', error);
|
|
return null;
|
|
}
|
|
}
|
|
} |