Fix: Correction de la boucle de redirection OAuth infinie
- Stockage des tokens dans des cookies HttpOnly côté serveur - Suppression du stockage localStorage côté client - Modification du middleware pour vérifier les cookies HttpOnly - Redirection propre après authentification - Suppression du nettoyage précoce des paramètres URL Cela corrige le problème où le dashboard se rafraîchissait en boucle après l'authentification Keycloak. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -75,9 +75,8 @@ const Dashboard = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Hooks pour les données et actions du dashboard
|
// Hooks pour les données et actions du dashboard
|
||||||
// Ne charger les données que si l'authentification est terminée ou qu'on a déjà des tokens
|
// Charger les données après authentification ou si pas de code d'autorisation
|
||||||
const hasTokens = typeof window !== 'undefined' && !!localStorage.getItem('accessToken');
|
const shouldLoadData = authProcessed || !currentCode;
|
||||||
const shouldLoadData = authProcessed || hasTokens || !currentCode;
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metrics,
|
metrics,
|
||||||
@@ -182,11 +181,6 @@ const Dashboard = () => {
|
|||||||
chantierActions.handleQuickView(chantier);
|
chantierActions.handleQuickView(chantier);
|
||||||
}, [chantierActions]);
|
}, [chantierActions]);
|
||||||
|
|
||||||
// Nettoyer les paramètres d'authentification au montage
|
|
||||||
useEffect(() => {
|
|
||||||
cleanAuthParams();
|
|
||||||
}, [cleanAuthParams]);
|
|
||||||
|
|
||||||
// Traiter l'authentification Keycloak si nécessaire
|
// Traiter l'authentification Keycloak si nécessaire
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Si l'authentification est déjà terminée, ne rien faire
|
// Si l'authentification est déjà terminée, ne rien faire
|
||||||
@@ -201,13 +195,9 @@ const Dashboard = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier si on a déjà des tokens valides
|
// Les tokens sont maintenant stockés dans des cookies HttpOnly
|
||||||
const hasTokens = localStorage.getItem('accessToken');
|
// Le middleware les vérifiera automatiquement
|
||||||
if (hasTokens) {
|
// Pas besoin de vérifier localStorage
|
||||||
console.log('✅ Tokens déjà présents, arrêt du processus d\'authentification');
|
|
||||||
setAuthProcessed(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = currentCode;
|
const code = currentCode;
|
||||||
const state = currentState;
|
const state = currentState;
|
||||||
@@ -229,14 +219,8 @@ const Dashboard = () => {
|
|||||||
processedCodeRef.current = code;
|
processedCodeRef.current = code;
|
||||||
setAuthInProgress(true);
|
setAuthInProgress(true);
|
||||||
|
|
||||||
// Nettoyer les anciens tokens avant l'échange
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('idToken');
|
|
||||||
|
|
||||||
console.log('📡 Appel API /api/auth/token...');
|
console.log('📡 Appel API /api/auth/token...');
|
||||||
|
|
||||||
|
|
||||||
const response = await fetch('/api/auth/token', {
|
const response = await fetch('/api/auth/token', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -292,28 +276,12 @@ const Dashboard = () => {
|
|||||||
throw new Error(`Échec de l'échange de token: ${errorText}`);
|
throw new Error(`Échec de l'échange de token: ${errorText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await response.json();
|
const result = await response.json();
|
||||||
console.log('✅ Tokens reçus dans le dashboard:', {
|
console.log('✅ Authentification réussie, tokens stockés dans des cookies HttpOnly');
|
||||||
hasAccessToken: !!tokens.access_token,
|
|
||||||
hasRefreshToken: !!tokens.refresh_token,
|
|
||||||
hasIdToken: !!tokens.id_token
|
|
||||||
});
|
|
||||||
|
|
||||||
// Réinitialiser le compteur de tentatives d'authentification
|
// Réinitialiser le compteur de tentatives d'authentification
|
||||||
localStorage.removeItem('auth_retry_count');
|
localStorage.removeItem('auth_retry_count');
|
||||||
|
|
||||||
// Stocker les tokens
|
|
||||||
if (tokens.access_token) {
|
|
||||||
localStorage.setItem('accessToken', tokens.access_token);
|
|
||||||
localStorage.setItem('refreshToken', tokens.refresh_token);
|
|
||||||
localStorage.setItem('idToken', tokens.id_token);
|
|
||||||
|
|
||||||
// Stocker aussi dans un cookie pour le middleware
|
|
||||||
document.cookie = `keycloak-token=${tokens.access_token}; path=/; max-age=3600; SameSite=Lax`;
|
|
||||||
|
|
||||||
console.log('✅ Tokens stockés avec succès');
|
|
||||||
}
|
|
||||||
|
|
||||||
setAuthProcessed(true);
|
setAuthProcessed(true);
|
||||||
setAuthInProgress(false);
|
setAuthInProgress(false);
|
||||||
authProcessingRef.current = false;
|
authProcessingRef.current = false;
|
||||||
@@ -327,13 +295,9 @@ const Dashboard = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nettoyer l'URL IMMÉDIATEMENT et arrêter tout traitement futur
|
// Nettoyer l'URL et recharger pour que le middleware vérifie les cookies
|
||||||
console.log('🧹 Dashboard: Nettoyage de l\'URL...');
|
console.log('🧹 Dashboard: Nettoyage de l\'URL et rechargement...');
|
||||||
window.history.replaceState({}, document.title, '/dashboard');
|
window.location.href = '/dashboard';
|
||||||
|
|
||||||
// Charger les données du dashboard
|
|
||||||
console.log('🔄 Dashboard: Chargement des données...');
|
|
||||||
refresh();
|
|
||||||
|
|
||||||
// Arrêter définitivement le processus d'authentification
|
// Arrêter définitivement le processus d'authentification
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -120,13 +120,44 @@ export async function POST(request: NextRequest) {
|
|||||||
hasIdToken: !!tokens.id_token
|
hasIdToken: !!tokens.id_token
|
||||||
});
|
});
|
||||||
|
|
||||||
// Supprimer le cookie du code verifier
|
// Créer la réponse avec les tokens
|
||||||
const response = NextResponse.json({
|
const response = NextResponse.json({
|
||||||
...tokens,
|
success: true,
|
||||||
returnUrl: '/dashboard' // URL par défaut, sera remplacée côté client si returnUrl existe
|
returnUrl: '/dashboard'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stocker les tokens dans des cookies HttpOnly sécurisés
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
response.cookies.set('keycloak-token', tokens.access_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: tokens.expires_in || 3600, // 1 heure par défaut
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
response.cookies.set('keycloak-refresh-token', tokens.refresh_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: tokens.refresh_expires_in || 86400, // 24 heures par défaut
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
response.cookies.set('keycloak-id-token', tokens.id_token, {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isProduction,
|
||||||
|
sameSite: 'lax',
|
||||||
|
maxAge: tokens.expires_in || 3600,
|
||||||
|
path: '/'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Supprimer le cookie du code verifier
|
||||||
response.cookies.delete('pkce_code_verifier');
|
response.cookies.delete('pkce_code_verifier');
|
||||||
|
|
||||||
|
console.log('🍪 Tokens stockés dans des cookies HttpOnly sécurisés');
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -123,13 +123,6 @@ export async function middleware(request: NextRequest) {
|
|||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignorer les requêtes vers /dashboard qui contiennent un code d'autorisation
|
|
||||||
// Cela permet à la page de traiter le code sans que le middleware ne redirige
|
|
||||||
if (pathname === '/dashboard' && request.nextUrl.searchParams.has('code')) {
|
|
||||||
console.log('🔓 Middleware: Autorisant /dashboard avec code d\'autorisation');
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Permettre l'accès aux routes publiques
|
// Permettre l'accès aux routes publiques
|
||||||
if (isPublicRoute(pathname)) {
|
if (isPublicRoute(pathname)) {
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
@@ -141,12 +134,13 @@ export async function middleware(request: NextRequest) {
|
|||||||
const authHeader = request.headers.get('authorization');
|
const authHeader = request.headers.get('authorization');
|
||||||
const tokenFromCookie = request.cookies.get('keycloak-token')?.value;
|
const tokenFromCookie = request.cookies.get('keycloak-token')?.value;
|
||||||
const pkceVerifier = request.cookies.get('pkce_code_verifier')?.value;
|
const pkceVerifier = request.cookies.get('pkce_code_verifier')?.value;
|
||||||
|
const hasAuthCode = request.nextUrl.searchParams.has('code');
|
||||||
|
|
||||||
console.log(`🔍 Middleware: Vérification de ${pathname}:`, {
|
console.log(`🔍 Middleware: Vérification de ${pathname}:`, {
|
||||||
hasAuthHeader: !!authHeader,
|
hasAuthHeader: !!authHeader,
|
||||||
hasTokenCookie: !!tokenFromCookie,
|
hasTokenCookie: !!tokenFromCookie,
|
||||||
hasPkceVerifier: !!pkceVerifier,
|
hasPkceVerifier: !!pkceVerifier,
|
||||||
hasCode: request.nextUrl.searchParams.has('code')
|
hasCode: hasAuthCode
|
||||||
});
|
});
|
||||||
|
|
||||||
let token: string | null = null;
|
let token: string | null = null;
|
||||||
@@ -159,14 +153,14 @@ export async function middleware(request: NextRequest) {
|
|||||||
|
|
||||||
// Si pas de token, vérifier si un processus d'authentification est en cours
|
// Si pas de token, vérifier si un processus d'authentification est en cours
|
||||||
if (!token) {
|
if (!token) {
|
||||||
// Si on a un code verifier PKCE, cela signifie qu'un processus d'authentification est en cours
|
// Autoriser l'accès SEULEMENT si on a un code d'autorisation ET un PKCE verifier
|
||||||
// Autoriser l'accès pour permettre à la page de terminer l'échange du code
|
// Cela permet le premier passage pour l'échange du code
|
||||||
if (pkceVerifier && pathname === '/dashboard') {
|
if (hasAuthCode && pkceVerifier && pathname === '/dashboard') {
|
||||||
console.log('🔓 Middleware: Autorisant /dashboard avec PKCE verifier (authentification en cours)');
|
console.log('🔓 Middleware: Autorisant /dashboard pour l\'échange du code d\'autorisation');
|
||||||
return NextResponse.next();
|
return NextResponse.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`🔒 Middleware: Redirection vers /api/auth/login pour ${pathname} (pas de token ni de PKCE verifier)`);
|
console.log(`🔒 Middleware: Redirection vers /api/auth/login pour ${pathname} (pas de token)`);
|
||||||
const loginUrl = new URL('/api/auth/login', request.url);
|
const loginUrl = new URL('/api/auth/login', request.url);
|
||||||
loginUrl.searchParams.set('redirect', pathname);
|
loginUrl.searchParams.set('redirect', pathname);
|
||||||
return NextResponse.redirect(loginUrl);
|
return NextResponse.redirect(loginUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user