From 418fac529ad7a117100fb2ff114b30bb5668396f Mon Sep 17 00:00:00 2001 From: dahoud Date: Sun, 19 Oct 2025 12:37:00 +0000 Subject: [PATCH] Fix: Correction de la boucle de redirection OAuth infinie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/(main)/dashboard/page.tsx | 56 +++++++---------------------------- app/api/auth/token/route.ts | 37 +++++++++++++++++++++-- middleware.ts | 20 +++++-------- 3 files changed, 51 insertions(+), 62 deletions(-) diff --git a/app/(main)/dashboard/page.tsx b/app/(main)/dashboard/page.tsx index 26589cb..3031798 100644 --- a/app/(main)/dashboard/page.tsx +++ b/app/(main)/dashboard/page.tsx @@ -75,9 +75,8 @@ const 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 - const hasTokens = typeof window !== 'undefined' && !!localStorage.getItem('accessToken'); - const shouldLoadData = authProcessed || hasTokens || !currentCode; + // Charger les données après authentification ou si pas de code d'autorisation + const shouldLoadData = authProcessed || !currentCode; const { metrics, @@ -182,11 +181,6 @@ const Dashboard = () => { chantierActions.handleQuickView(chantier); }, [chantierActions]); - // Nettoyer les paramètres d'authentification au montage - useEffect(() => { - cleanAuthParams(); - }, [cleanAuthParams]); - // Traiter l'authentification Keycloak si nécessaire useEffect(() => { // Si l'authentification est déjà terminée, ne rien faire @@ -201,13 +195,9 @@ const Dashboard = () => { return; } - // Vérifier si on a déjà des tokens valides - const hasTokens = localStorage.getItem('accessToken'); - if (hasTokens) { - console.log('✅ Tokens déjà présents, arrêt du processus d\'authentification'); - setAuthProcessed(true); - return; - } + // Les tokens sont maintenant stockés dans des cookies HttpOnly + // Le middleware les vérifiera automatiquement + // Pas besoin de vérifier localStorage const code = currentCode; const state = currentState; @@ -229,14 +219,8 @@ const Dashboard = () => { processedCodeRef.current = code; 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...'); - const response = await fetch('/api/auth/token', { method: 'POST', headers: { @@ -292,28 +276,12 @@ const Dashboard = () => { throw new Error(`Échec de l'échange de token: ${errorText}`); } - const tokens = await response.json(); - console.log('✅ Tokens reçus dans le dashboard:', { - hasAccessToken: !!tokens.access_token, - hasRefreshToken: !!tokens.refresh_token, - hasIdToken: !!tokens.id_token - }); + const result = await response.json(); + console.log('✅ Authentification réussie, tokens stockés dans des cookies HttpOnly'); // Réinitialiser le compteur de tentatives d'authentification 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); setAuthInProgress(false); authProcessingRef.current = false; @@ -327,13 +295,9 @@ const Dashboard = () => { return; } - // Nettoyer l'URL IMMÉDIATEMENT et arrêter tout traitement futur - console.log('🧹 Dashboard: Nettoyage de l\'URL...'); - window.history.replaceState({}, document.title, '/dashboard'); - - // Charger les données du dashboard - console.log('🔄 Dashboard: Chargement des données...'); - refresh(); + // Nettoyer l'URL et recharger pour que le middleware vérifie les cookies + console.log('🧹 Dashboard: Nettoyage de l\'URL et rechargement...'); + window.location.href = '/dashboard'; // Arrêter définitivement le processus d'authentification return; diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts index a0663f2..de68de2 100644 --- a/app/api/auth/token/route.ts +++ b/app/api/auth/token/route.ts @@ -120,13 +120,44 @@ export async function POST(request: NextRequest) { hasIdToken: !!tokens.id_token }); - // Supprimer le cookie du code verifier + // Créer la réponse avec les tokens const response = NextResponse.json({ - ...tokens, - returnUrl: '/dashboard' // URL par défaut, sera remplacée côté client si returnUrl existe + success: true, + 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'); + console.log('🍪 Tokens stockés dans des cookies HttpOnly sécurisés'); + return response; } catch (error) { diff --git a/middleware.ts b/middleware.ts index 6cdcbab..54fd9cc 100644 --- a/middleware.ts +++ b/middleware.ts @@ -123,13 +123,6 @@ export async function middleware(request: NextRequest) { 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 if (isPublicRoute(pathname)) { return NextResponse.next(); @@ -141,12 +134,13 @@ export async function middleware(request: NextRequest) { const authHeader = request.headers.get('authorization'); const tokenFromCookie = request.cookies.get('keycloak-token')?.value; const pkceVerifier = request.cookies.get('pkce_code_verifier')?.value; + const hasAuthCode = request.nextUrl.searchParams.has('code'); console.log(`🔍 Middleware: Vérification de ${pathname}:`, { hasAuthHeader: !!authHeader, hasTokenCookie: !!tokenFromCookie, hasPkceVerifier: !!pkceVerifier, - hasCode: request.nextUrl.searchParams.has('code') + hasCode: hasAuthCode }); 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 if (!token) { - // Si on a un code verifier PKCE, cela signifie qu'un processus d'authentification est en cours - // Autoriser l'accès pour permettre à la page de terminer l'échange du code - if (pkceVerifier && pathname === '/dashboard') { - console.log('🔓 Middleware: Autorisant /dashboard avec PKCE verifier (authentification en cours)'); + // Autoriser l'accès SEULEMENT si on a un code d'autorisation ET un PKCE verifier + // Cela permet le premier passage pour l'échange du code + if (hasAuthCode && pkceVerifier && pathname === '/dashboard') { + console.log('🔓 Middleware: Autorisant /dashboard pour l\'échange du code d\'autorisation'); 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); loginUrl.searchParams.set('redirect', pathname); return NextResponse.redirect(loginUrl);