NeahNew/ACTION_PLAN_LOGIN_FLOW.md
2026-01-04 10:32:31 +01:00

426 lines
12 KiB
Markdown

# 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
<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 :
```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