[INT-AUTH-003] Verify refresh token flow handles edge cases
This commit is contained in:
parent
c70dc23e70
commit
25c38d10b7
2 changed files with 103 additions and 13 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue