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

12 KiB

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 :

// 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.tsx
  • components/main-nav.tsx
  • components/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=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