12 KiB
Plan d'Action - Amélioration Flow de Connexion
🎯 Objectifs
- Améliorer l'UX : Permettre SSO naturel pour les utilisateurs légitimes
- Sécuriser le logout : S'assurer que les credentials sont demandés après logout
- Simplifier le code : Réduire la complexité de détection session invalide
- É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 :
// 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
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) :
// 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 :
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) :
<button
onClick={() => {
// Forcer prompt=login en ajoutant un paramètre
// Note: NextAuth ne supporte pas prompt directement dans signIn()
// Solution: Utiliser un paramètre custom dans l'URL
const url = new URL(window.location.origin + '/api/auth/signin/keycloak');
url.searchParams.set('callbackUrl', '/');
url.searchParams.set('force_login', 'true');
window.location.href = url.toString();
}}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Se connecter
</button>
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 :
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
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 :
// 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 :
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 :
// 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 :
// 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 :
// 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: { ... } :
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.tsxcomponents/main-nav.tsxcomponents/layout/layout-wrapper.tsx
Changement (ligne ~58-76) :
// 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=loginpar 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