[INT-AUTH-004] Add token expiration pre-check

This commit is contained in:
senke 2025-12-26 09:15:00 +01:00
parent 25c38d10b7
commit c171d66b0c
3 changed files with 71 additions and 12 deletions

View file

@ -650,7 +650,8 @@
"description": "Vérifier l'expiration du token AVANT d'envoyer une requête pour éviter 401 inutiles.",
"priority": "P2",
"priority_rank": 19,
"status": "todo",
"status": "completed",
"completed_at": "2025-01-27T16:15:00Z",
"estimated_hours": 1.5,
"side": "frontend_only",
"files_to_modify": [
@ -1100,13 +1101,13 @@
},
"progress_tracking": {
"total_tasks": 32,
"completed": 18,
"completed": 19,
"in_progress": 0,
"todo": 14,
"todo": 13,
"blocked": 0,
"completion_percentage": 56,
"last_updated": "2025-01-27T16:00:00Z",
"completion_percentage": 59,
"last_updated": "2025-01-27T16:15:00Z",
"estimated_completion_date": null,
"estimated_hours_remaining": 21
"estimated_hours_remaining": 19.5
}
}

View file

@ -2,7 +2,7 @@ import axios, { AxiosError, InternalAxiosRequestConfig, AxiosResponse } from 'ax
import toast from 'react-hot-toast';
import { z } from 'zod';
import { TokenStorage } from '../tokenStorage';
import { refreshToken } from '../tokenRefresh';
import { refreshToken, isTokenExpiringSoon } from '../tokenRefresh';
import { env } from '@/config/env';
import { parseApiError } from '@/utils/apiErrorHandler';
import { csrfService } from '../csrf';
@ -208,10 +208,67 @@ const processQueue = (error: Error | null, token: string | null = null) => {
// T0177: Interceptor de requête pour ajouter le token d'accès
// CRITIQUE: Récupérer TOUJOURS le token frais depuis localStorage car Zustand peut ne pas être hydraté
apiClient.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
async (config: InternalAxiosRequestConfig) => {
// INT-AUTH-004: Vérifier l'expiration du token avant d'envoyer la requête
// Buffer de 60 secondes pour éviter les 401 inutiles
const PRE_REQUEST_REFRESH_BUFFER_MS = 60 * 1000; // 60 secondes
const token = TokenStorage.getAccessToken();
if (token && config.headers) {
const isRefreshEndpoint = config.url?.includes('/auth/refresh');
const isCSRFRoute = config.url?.includes('/csrf-token');
// Ne pas vérifier l'expiration pour les endpoints de refresh et CSRF pour éviter les boucles
if (token && !isRefreshEndpoint && !isCSRFRoute) {
// Vérifier si le token expire bientôt (dans moins de 60s)
if (isTokenExpiringSoon(token, PRE_REQUEST_REFRESH_BUFFER_MS)) {
// Si un refresh est déjà en cours, attendre qu'il se termine
if (isRefreshing) {
logger.debug('[API] Token expiring soon but refresh already in progress, waiting...', {
url: config.url,
});
// Attendre que le refresh se termine (max 5s)
let waitCount = 0;
while (isRefreshing && waitCount < 50) {
await new Promise(resolve => setTimeout(resolve, 100));
waitCount++;
}
// Récupérer le nouveau token après le refresh
const newToken = TokenStorage.getAccessToken();
if (newToken && config.headers) {
config.headers.Authorization = `Bearer ${newToken} `;
}
} else {
// Rafraîchir proactivement le token
try {
logger.debug('[API] Token expiring soon, refreshing proactively before request', {
url: config.url,
buffer_seconds: PRE_REQUEST_REFRESH_BUFFER_MS / 1000,
});
await refreshToken();
const newToken = TokenStorage.getAccessToken();
if (newToken && config.headers) {
config.headers.Authorization = `Bearer ${newToken} `;
}
} catch (refreshError) {
// Si le refresh échoue, continuer avec le token actuel
// L'interceptor de réponse gérera l'erreur 401 si nécessaire
logger.warn('[API] Proactive token refresh failed, continuing with current token', {
url: config.url,
error: refreshError,
});
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token} `;
}
}
}
} else {
// Token valide, utiliser normalement
if (config.headers) {
config.headers.Authorization = `Bearer ${token} `;
}
}
} else if (token && config.headers) {
// Token présent mais endpoint de refresh/CSRF, utiliser sans vérification
config.headers.Authorization = `Bearer ${token} `;
}
@ -225,7 +282,7 @@ apiClient.interceptors.request.use(
// INT-AUTH-001: Ajouter le token CSRF pour les méthodes qui modifient l'état
const method = config.method?.toUpperCase();
const isStateChanging = ['POST', 'PUT', 'DELETE', 'PATCH'].includes(method || '');
const isCSRFRoute = config.url?.includes('/csrf-token');
// isCSRFRoute déjà défini plus haut
if (isStateChanging && !isCSRFRoute && config.headers) {
const csrfToken = csrfService.getToken();

View file

@ -84,11 +84,12 @@ function decodeJWT(token: string): JWTPayload | null {
/**
* INT-016: Vérifie si un token est expiré ou proche de l'expiration
* INT-AUTH-004: Exporté pour utilisation dans l'interceptor de requête
* @param token - Token JWT à vérifier
* @param bufferMs - Buffer en millisecondes avant expiration (défaut: 5 minutes)
* @returns true si le token est expiré ou proche de l'expiration
*/
function isTokenExpiringSoon(token: string | null, bufferMs: number = PROACTIVE_REFRESH_BUFFER_MS): boolean {
export function isTokenExpiringSoon(token: string | null, bufferMs: number = PROACTIVE_REFRESH_BUFFER_MS): boolean {
if (!token) {
return true;
}