426 lines
12 KiB
Markdown
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
|
|
|