[INT-AUTH-003] Verify refresh token flow handles edge cases

This commit is contained in:
senke 2025-12-26 09:13:36 +01:00
parent c70dc23e70
commit 25c38d10b7
2 changed files with 103 additions and 13 deletions

View file

@ -620,7 +620,8 @@
"description": "S'assurer que le refresh token gère: expiration, révocation, erreurs réseau.",
"priority": "P1",
"priority_rank": 18,
"status": "todo",
"status": "completed",
"completed_at": "2025-01-27T16:00:00Z",
"estimated_hours": 2,
"side": "frontend_only",
"files_to_modify": [
@ -1099,13 +1100,13 @@
},
"progress_tracking": {
"total_tasks": 32,
"completed": 17,
"completed": 18,
"in_progress": 0,
"todo": 15,
"todo": 14,
"blocked": 0,
"completion_percentage": 53,
"last_updated": "2025-01-27T15:45:00Z",
"completion_percentage": 56,
"last_updated": "2025-01-27T16:00:00Z",
"estimated_completion_date": null,
"estimated_hours_remaining": 23
"estimated_hours_remaining": 21
}
}

View file

@ -500,19 +500,61 @@ apiClient.interceptors.response.use(
});
}
// Détecter 401 et refresh automatiquement
// INT-AUTH-003: Détecter 401 et refresh automatiquement
// EXCLURE l'endpoint /auth/refresh pour éviter les boucles infinies
const isRefreshEndpoint = originalRequest?.url?.includes('/auth/refresh');
// INT-AUTH-003: Handle 401 on /auth/refresh endpoint - token expired/revoked, logout and redirect
if (error.response?.status === 401 && isRefreshEndpoint) {
logger.error('[API] 401 on /auth/refresh - refresh token expired or revoked, logging out', {
request_id: requestId,
url: originalRequest?.url,
});
// Clear tokens
TokenStorage.clearTokens();
// Clear CSRF token
csrfService.clearToken();
// Clear auth store state
if (typeof window !== 'undefined') {
// Import and use auth store to clear state
import('@/features/auth/store/authStore').then(({ useAuthStore }) => {
const store = useAuthStore.getState();
store.logout().catch((err) => {
logger.error('[API] Failed to logout from store after refresh token 401', { error: err });
});
}).catch((err) => {
logger.error('[API] Failed to import auth store for logout', { error: err });
});
// Store error message for display after redirect
sessionStorage.setItem(
'auth_error',
'Votre session a expiré. Veuillez vous reconnecter.',
);
// Redirect to login
window.location.href = '/login';
}
return Promise.reject(parseApiError(error));
}
if (
error.response?.status === 401 &&
originalRequest &&
!originalRequest._retry &&
!isRefreshEndpoint
) {
// Éviter les refresh multiples simultanés
// INT-AUTH-003: Éviter les refresh multiples simultanés
if (isRefreshing) {
// Si un refresh est en cours, mettre la requête en queue
logger.debug('[API] Refresh already in progress, queuing request', {
request_id: requestId,
url: originalRequest?.url,
queue_size: failedQueue.length,
});
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
})
@ -520,9 +562,18 @@ apiClient.interceptors.response.use(
if (originalRequest.headers && token) {
originalRequest.headers.Authorization = `Bearer ${token} `;
}
logger.debug('[API] Replaying queued request after successful refresh', {
request_id: requestId,
url: originalRequest?.url,
});
return apiClient(originalRequest);
})
.catch((err) => {
logger.error('[API] Queued request failed after refresh', {
request_id: requestId,
url: originalRequest?.url,
error: err,
});
return Promise.reject(err);
});
}
@ -530,8 +581,14 @@ apiClient.interceptors.response.use(
originalRequest._retry = true;
isRefreshing = true;
logger.info('[API] Starting token refresh due to 401', {
request_id: requestId,
url: originalRequest?.url,
method: originalRequest?.method,
});
try {
// Refresh automatique du token
// INT-AUTH-003: Refresh automatique du token
await refreshToken();
const newToken = TokenStorage.getAccessToken();
@ -539,25 +596,53 @@ apiClient.interceptors.response.use(
throw new Error('Failed to get new access token after refresh');
}
logger.info('[API] Token refresh successful, retrying original request', {
request_id: requestId,
url: originalRequest?.url,
queue_size: failedQueue.length,
});
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken} `;
}
// Traiter la queue et retry la requête originale
// INT-AUTH-003: Traiter la queue et retry la requête originale
// Toutes les requêtes en queue seront rejouées avec le nouveau token
processQueue(null, newToken);
return apiClient(originalRequest);
} catch (refreshError) {
// Gérer cas refresh échoué
// INT-AUTH-003: Gérer cas refresh échoué (expiration, révocation, erreur réseau)
logger.error('[API] Token refresh failed, logging out', {
request_id: requestId,
error: refreshError,
queue_size: failedQueue.length,
});
// Rejeter toutes les requêtes en queue
processQueue(refreshError as Error, null);
// Nettoyer les tokens
TokenStorage.clearTokens();
// Stocker un message d'erreur pour l'afficher après redirection
// Clear CSRF token
csrfService.clearToken();
// INT-AUTH-003: Clear auth store state and redirect to login
if (typeof window !== 'undefined') {
// Import and use auth store to clear state
import('@/features/auth/store/authStore').then(({ useAuthStore }) => {
const store = useAuthStore.getState();
store.logout().catch((err) => {
logger.error('[API] Failed to logout from store after refresh failure', { error: err });
});
}).catch((err) => {
logger.error('[API] Failed to import auth store for logout', { error: err });
});
// Stocker un message d'erreur pour l'afficher après redirection
sessionStorage.setItem(
'auth_error',
'Your session has expired. Please log in again.',
'Votre session a expiré. Veuillez vous reconnecter.',
);
// Rediriger vers login si refresh échoue (seulement dans le navigateur)
window.location.href = '/login';
@ -566,6 +651,10 @@ apiClient.interceptors.response.use(
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
logger.debug('[API] Token refresh process completed', {
request_id: requestId,
is_refreshing: false,
});
}
}