# Plan d'Action - Amélioration Flow de Connexion ## 🎯 Objectifs 1. **Améliorer l'UX** : Permettre SSO naturel pour les utilisateurs légitimes 2. **Sécuriser le logout** : S'assurer que les credentials sont demandés après logout 3. **Simplifier le code** : Réduire la complexité de détection session invalide 4. **Éliminer les race conditions** : Mécanisme robuste pour éviter auto-login après logout --- ## 📋 Actions Immédiates (À faire en premier) ### Action 1 : Supprimer `prompt=login` par défaut ⚡ **Fichier** : `app/api/auth/options.ts` **Changement** : ```typescript // AVANT (ligne 147-155) authorization: { params: { scope: "openid profile email roles", prompt: "login" // ❌ Supprimer cette ligne } } // APRÈS authorization: { params: { scope: "openid profile email roles", // prompt: "login" supprimé - sera ajouté conditionnellement après logout } } ``` **Impact** : ✅ SSO fonctionne naturellement pour les utilisateurs légitimes --- ### Action 2 : Créer route API pour marquer logout ⚡ **Nouveau fichier** : `app/api/auth/mark-logout/route.ts` ```typescript import { NextRequest, NextResponse } from 'next/server'; export async function POST(request: NextRequest) { const response = NextResponse.json({ success: true, message: 'Logout marked successfully' }); // Cookie HttpOnly pour marquer le logout (5 minutes) response.cookies.set('force_login_prompt', 'true', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: 300 // 5 minutes }); return response; } ``` **Impact** : ✅ Mécanisme robuste pour forcer login après logout --- ### Action 3 : Modifier signout-handler pour utiliser la route ⚡ **Fichier** : `components/auth/signout-handler.tsx` **Changement** (après ligne 25) : ```typescript // AVANT clearKeycloakCookies(); // APRÈS clearKeycloakCookies(); // Marquer le logout côté serveur try { await fetch('/api/auth/mark-logout', { method: 'POST', credentials: 'include', }); } catch (error) { console.error('Error marking logout:', error); // Continue même si ça échoue } ``` **Répéter dans** : - `components/main-nav.tsx` (ligne ~377) - `components/layout/layout-wrapper.tsx` (ligne ~42) **Impact** : ✅ Flag serveur pour empêcher auto-login --- ### Action 4 : Simplifier signin/page.tsx ⚡ **Fichier** : `app/signin/page.tsx` **Changement** : Remplacer la logique complexe (lignes 17-67) par : ```typescript useEffect(() => { // Vérifier le cookie serveur pour forcer login const forceLoginCookie = document.cookie .split(';') .find(c => c.trim().startsWith('force_login_prompt=')); // Si logout récent, forcer prompt=login if (forceLoginCookie) { // Supprimer le cookie document.cookie = 'force_login_prompt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; // Ne pas auto-login, attendre clic utilisateur // Le bouton "Se connecter" forcera prompt=login return; } // Si déjà authentifié, rediriger if (status === "authenticated" && session?.user) { router.push("/"); return; } // Si non authentifié et pas de flag logout, auto-login (SSO naturel) if (status === "unauthenticated" && !forceLoginCookie) { const timer = setTimeout(() => { if (status === "unauthenticated") { signIn("keycloak", { callbackUrl: "/" }); } }, 1000); return () => clearTimeout(timer); } }, [status, session, router]); ``` **ET** modifier le bouton "Se connecter" (ligne ~202) : ```typescript ``` **Impact** : ✅ Code plus simple et maintenable --- ### Action 5 : Ajouter prompt=login conditionnel dans options.ts ⚡ **Fichier** : `app/api/auth/options.ts` **Changement** : Modifier la configuration KeycloakProvider pour accepter un paramètre custom : ```typescript KeycloakProvider({ clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), authorization: { params: { scope: "openid profile email roles", // prompt sera ajouté dynamiquement si force_login=true dans l'URL } }, // ... profile callback ... }) ``` **ET** créer une route custom pour signin qui ajoute prompt : **Nouveau fichier** : `app/api/auth/signin/keycloak/route.ts` ```typescript import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '../../options'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const forceLogin = searchParams.get('force_login') === 'true'; const callbackUrl = searchParams.get('callbackUrl') || '/'; // Rediriger vers NextAuth signin avec prompt si nécessaire const signinUrl = new URL('/api/auth/signin/keycloak', request.nextUrl.origin); signinUrl.searchParams.set('callbackUrl', callbackUrl); if (forceLogin) { // Ajouter prompt=login dans l'URL de redirection Keycloak // Note: NextAuth ne supporte pas directement, il faut modifier l'URL après // Solution alternative: Utiliser un middleware ou modifier options dynamiquement } return NextResponse.redirect(signinUrl); } ``` **OU** Solution plus simple : Modifier directement dans `options.ts` pour lire un cookie : ```typescript // Dans options.ts, modifier authorization params dynamiquement authorization: { params: (provider, action, request) => { const forceLogin = request?.cookies?.get('force_login_prompt')?.value === 'true'; return { scope: "openid profile email roles", ...(forceLogin ? { prompt: "login" } : {}), }; } } ``` **Note** : NextAuth v4 ne supporte pas `params` comme fonction. Solution alternative : **Modifier** `app/api/auth/options.ts` pour utiliser `authorization.url` : ```typescript KeycloakProvider({ // ... config ... authorization: { params: { scope: "openid profile email roles", }, // Ajouter prompt dynamiquement via URL personnalisée url: (params) => { // Vérifier si on doit forcer login (via cookie ou autre moyen) const url = new URL(`${process.env.KEYCLOAK_ISSUER}/protocol/openid-connect/auth`); url.searchParams.set('client_id', process.env.KEYCLOAK_CLIENT_ID!); url.searchParams.set('redirect_uri', params.redirect_uri); url.searchParams.set('response_type', 'code'); url.searchParams.set('scope', 'openid profile email roles'); url.searchParams.set('state', params.state); // Ajouter prompt si nécessaire (à vérifier via cookie dans le callback) // Note: Plus complexe, nécessite de passer le flag via state return url.toString(); } } }) ``` **Solution RECOMMANDÉE (plus simple)** : Utiliser un paramètre dans l'URL de callback et le vérifier dans le callback JWT : ```typescript // Dans signin/page.tsx, lors du clic sur "Se connecter" const url = new URL(window.location.origin + '/api/auth/signin/keycloak'); url.searchParams.set('callbackUrl', '/'); url.searchParams.set('force_login', 'true'); // Stocker dans sessionStorage pour le callback sessionStorage.setItem('force_login', 'true'); window.location.href = url.toString(); // Dans options.ts, callback jwt, vérifier sessionStorage n'est pas possible côté serveur // Solution: Passer via state OAuth ``` **MEILLEURE SOLUTION** : Utiliser un cookie avant le signIn : ```typescript // Dans signin/page.tsx, bouton "Se connecter" onClick={() => { // Créer cookie pour forcer login document.cookie = 'force_login_prompt=true; path=/; max-age=300'; // Puis signIn normal signIn("keycloak", { callbackUrl: "/" }); }} // Dans options.ts, lire le cookie dans authorization params // Note: NextAuth ne permet pas d'accéder aux cookies dans params // Solution: Middleware ou route custom ``` **SOLUTION FINALE RECOMMANDÉE** : Créer une route API custom qui gère le signin avec prompt conditionnel : ```typescript // app/api/auth/custom-signin/route.ts import { NextRequest, NextResponse } from 'next/server'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const forceLogin = searchParams.get('force_login') === 'true'; const callbackUrl = searchParams.get('callbackUrl') || '/'; // Construire l'URL Keycloak avec prompt si nécessaire const keycloakIssuer = process.env.KEYCLOAK_ISSUER!; const clientId = process.env.KEYCLOAK_CLIENT_ID!; const redirectUri = `${request.nextUrl.origin}/api/auth/callback/keycloak`; const authUrl = new URL(`${keycloakIssuer}/protocol/openid-connect/auth`); authUrl.searchParams.set('client_id', clientId); authUrl.searchParams.set('redirect_uri', redirectUri); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('scope', 'openid profile email roles'); authUrl.searchParams.set('state', generateState()); // Générer state if (forceLogin) { authUrl.searchParams.set('prompt', 'login'); } return NextResponse.redirect(authUrl.toString()); } ``` **Impact** : ✅ Prompt login seulement après logout --- ## 🔧 Actions Secondaires (Après les actions immédiates) ### Action 6 : Configurer explicitement les cookies NextAuth **Fichier** : `app/api/auth/options.ts` **Ajouter** après `session: { ... }` : ```typescript cookies: { sessionToken: { name: `next-auth.session-token`, options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NEXTAUTH_URL?.startsWith('https://') ?? false, }, }, // ... autres cookies si nécessaire }, ``` --- ### Action 7 : Améliorer Keycloak logout URL **Fichiers** : - `components/auth/signout-handler.tsx` - `components/main-nav.tsx` - `components/layout/layout-wrapper.tsx` **Changement** (ligne ~58-76) : ```typescript // AVANT keycloakLogoutUrl.searchParams.append('kc_action', 'LOGOUT'); // APRÈS keycloakLogoutUrl.searchParams.append('kc_action', 'LOGOUT'); // Ajouter client_id pour forcer logout client spécifique if (process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID) { keycloakLogoutUrl.searchParams.append('client_id', process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID); } ``` --- ### Action 8 : Améliorer gestion erreur refresh token **Fichier** : `app/api/auth/options.ts` **Changement** : Voir détails dans `IMPROVEMENTS_LOGIN_FLOW.md` section "Problème 7" --- ## ✅ Checklist d'Implémentation ### Phase 1 : Corrections Critiques (1-2 heures) - [ ] Action 1 : Supprimer `prompt=login` par défaut - [ ] Action 2 : Créer route `/api/auth/mark-logout` - [ ] Action 3 : Modifier signout-handler pour utiliser la route - [ ] Action 4 : Simplifier signin/page.tsx - [ ] Action 5 : Ajouter prompt=login conditionnel ### Phase 2 : Améliorations (1 heure) - [ ] Action 6 : Configurer explicitement les cookies - [ ] Action 7 : Améliorer Keycloak logout URL - [ ] Action 8 : Améliorer gestion erreur refresh ### Phase 3 : Tests (30 minutes) - [ ] Tester login première visite (SSO doit fonctionner) - [ ] Tester login après logout (credentials doivent être demandés) - [ ] Tester logout depuis dashboard - [ ] Tester logout depuis iframe - [ ] Tester expiration session --- ## 🎯 Résultat Attendu ### Avant - ❌ Toujours demander credentials (même première visite) - ❌ Logique complexe de détection session invalide - ❌ Race conditions possibles - ❌ Cookies Keycloak peuvent persister ### Après - ✅ SSO naturel pour utilisateurs légitimes - ✅ Credentials demandés seulement après logout - ✅ Détection session invalide simple et robuste - ✅ Pas de race conditions - ✅ Meilleure gestion des cookies --- **Document créé le** : $(date) **Priorité** : Actions immédiates à faire en premier