diff --git a/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md b/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md
new file mode 100644
index 00000000..98e30f87
--- /dev/null
+++ b/IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md
@@ -0,0 +1,259 @@
+# Iframe Logout Auto-Login Issue Analysis
+
+## Problem
+
+When you log out from an iframe application, you are automatically logged back into the dashboard without being prompted for credentials.
+
+## Flow Trace
+
+### Scenario: User Logs Out from Iframe Application
+
+#### Step 1: Iframe Application Logout
+```
+Location: Iframe application (e.g., /parole, /gite, etc.)
+Action: User clicks logout in iframe
+
+What happens:
+- Iframe app may call Keycloak logout endpoint directly
+- OR: Iframe app sends postMessage to parent: { type: 'KEYCLOAK_LOGOUT' }
+- OR: Iframe app clears its own session cookies
+```
+
+#### Step 2A: If Iframe Sends PostMessage (Expected Flow)
+```
+Location: components/layout/layout-wrapper.tsx (line 26-106)
+OR: app/components/responsive-iframe.tsx (line 110-153)
+
+Action: Dashboard receives logout message
+
+What happens:
+1. Sets sessionStorage.setItem('just_logged_out', 'true')
+2. Sets document.cookie = 'logout_in_progress=true; path=/; max-age=60'
+3. Calls /api/auth/end-sso-session (Admin API)
+4. Calls signOut() from NextAuth
+5. Redirects to Keycloak logout endpoint
+6. Keycloak redirects back to /signin?logout=true
+```
+
+#### Step 2B: If Iframe Calls Keycloak Logout Directly (Actual Flow - Problem)
+```
+Location: Iframe application
+
+Action: Iframe calls Keycloak logout endpoint directly
+
+What happens:
+1. Iframe redirects to: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout
+2. Keycloak clears session cookies
+3. Keycloak may redirect iframe back to its own logout page
+4. Dashboard doesn't know about this logout
+5. Dashboard still has NextAuth session (valid for 30 days)
+```
+
+#### Step 3: Dashboard Detects Session Invalidation
+```
+Location: app/api/auth/options.ts (refreshAccessToken function)
+
+When: NextAuth tries to refresh the access token
+
+What happens:
+1. Dashboard calls Keycloak token refresh endpoint
+2. Keycloak returns: { error: 'invalid_grant', error_description: 'Session not active' }
+3. refreshAccessToken detects this error (line 100-108)
+4. Returns token with error: "SessionNotActive"
+5. JWT callback clears tokens (line 248-256)
+6. Session callback returns null (line 272-276)
+7. NextAuth treats user as unauthenticated
+8. Status becomes "unauthenticated"
+```
+
+#### Step 4: Sign-In Page Auto-Login (THE PROBLEM)
+```
+Location: app/signin/page.tsx (line 47-79)
+
+When: User is redirected to /signin (or status becomes "unauthenticated")
+
+What happens:
+1. Component mounts
+2. First useEffect (line 16-45) checks for logout flag
+ - If logout=true in URL, sets isLogoutRedirect.current = true
+ - Removes 'just_logged_out' from sessionStorage
+3. Second useEffect (line 47-79) checks authentication status
+ - If status === "authenticated" → redirects to home ✅
+ - If status === "unauthenticated" → triggers auto-login ❌
+
+THE PROBLEM:
+- When iframe logs out directly (not via postMessage), dashboard doesn't set logout flags
+- Status becomes "unauthenticated" (because Keycloak session was cleared)
+- Sign-in page sees status === "unauthenticated"
+- Auto-login logic triggers after 1 second (line 69)
+- signIn("keycloak") is called
+- Keycloak still has SSO session cookie (if it wasn't fully cleared)
+- User is auto-authenticated without credentials ❌
+```
+
+## Root Cause Analysis
+
+### Problem 1: Missing Logout Flags
+
+**When iframe logs out directly (not via postMessage):**
+- Dashboard doesn't know about the logout
+- `just_logged_out` is NOT set in sessionStorage
+- `logout_in_progress` cookie is NOT set
+- Sign-in page doesn't know this is a logout scenario
+
+**Result**: Sign-in page treats it as a normal "unauthenticated" state and triggers auto-login.
+
+### Problem 2: Auto-Login Logic Timing
+
+**Sign-in page auto-login logic** (`app/signin/page.tsx:66-78`):
+```typescript
+if (status === "unauthenticated") {
+ hasAttemptedLogin.current = true;
+ const timer = setTimeout(() => {
+ if (!isLogoutRedirect.current) {
+ signIn("keycloak", { callbackUrl: "/" });
+ }
+ }, 1000);
+}
+```
+
+**The Issue**:
+- `isLogoutRedirect.current` is set in the first useEffect (line 16-45)
+- But it only checks for `logout=true` in URL or `just_logged_out` in sessionStorage
+- If iframe logs out directly, neither of these is set
+- After 1 second, auto-login triggers
+- `isLogoutRedirect.current` is still `false` (because logout flags weren't set)
+- `signIn("keycloak")` is called
+- User is auto-authenticated
+
+### Problem 3: SSO Session Cookie Persistence
+
+**Even if logout flags are set correctly:**
+- Keycloak SSO session cookie (`KEYCLOAK_SESSION`) may still exist
+- When `signIn("keycloak")` is called, Keycloak checks for SSO session cookie
+- If cookie exists, Keycloak auto-authenticates without credentials
+- This happens even with `prompt=login` parameter (if SSO session is still valid)
+
+## Why This Happens
+
+### Flow 1: Iframe Logs Out via PostMessage (Works Correctly)
+```
+1. Iframe sends postMessage → Dashboard receives it
+2. Dashboard sets logout flags ✅
+3. Dashboard calls logout endpoints ✅
+4. Redirects to /signin?logout=true ✅
+5. Sign-in page sees logout=true ✅
+6. Auto-login is prevented ✅
+7. User must click "Se connecter" manually ✅
+```
+
+### Flow 2: Iframe Logs Out Directly (THE PROBLEM)
+```
+1. Iframe calls Keycloak logout directly
+2. Keycloak clears session cookies
+3. Dashboard doesn't know about logout ❌
+4. NextAuth tries to refresh token
+5. Keycloak returns "Session not active"
+6. NextAuth marks user as unauthenticated
+7. User is redirected to /signin (no logout=true) ❌
+8. Sign-in page sees status="unauthenticated" ❌
+9. Auto-login triggers after 1 second ❌
+10. Keycloak still has SSO session cookie ❌
+11. User is auto-authenticated ❌
+```
+
+## The Real Issue
+
+**The sign-in page auto-login logic is too aggressive:**
+
+1. It triggers auto-login for ANY "unauthenticated" state
+2. It doesn't distinguish between:
+ - User never logged in (should auto-login) ✅
+ - User logged out (should NOT auto-login) ❌
+ - Session expired (should NOT auto-login) ❌
+ - Keycloak session invalidated (should NOT auto-login) ❌
+
+3. The logout detection only works if:
+ - `logout=true` is in URL (from Keycloak redirect)
+ - `just_logged_out` is in sessionStorage (from dashboard logout)
+ - But NOT if iframe logs out directly
+
+## Solution Requirements
+
+To fix this issue, you need to:
+
+1. **Detect Keycloak Session Invalidation**:
+ - When NextAuth detects "SessionNotActive" error
+ - Set a flag to prevent auto-login
+ - Mark this as a logout scenario, not a new login
+
+2. **Improve Logout Detection**:
+ - Check for Keycloak session cookie existence
+ - If session was invalidated (not just expired), prevent auto-login
+ - Store logout reason in sessionStorage
+
+3. **Modify Auto-Login Logic**:
+ - Only auto-login if:
+ - User is truly unauthenticated (never logged in)
+ - AND no logout flags are set
+ - AND no session invalidation detected
+ - Don't auto-login if:
+ - Logout flags are set
+ - Session was invalidated
+ - User came from a logout flow
+
+4. **Handle Iframe Direct Logout**:
+ - Detect when Keycloak session is invalidated
+ - Set logout flags automatically
+ - Prevent auto-login
+
+## Current Code Issues
+
+### Issue 1: Auto-Login Logic (`app/signin/page.tsx:66-78`)
+```typescript
+if (status === "unauthenticated") {
+ // This triggers for ANY unauthenticated state
+ // Doesn't check if session was invalidated
+ signIn("keycloak", { callbackUrl: "/" });
+}
+```
+
+### Issue 2: Logout Detection (`app/signin/page.tsx:16-45`)
+```typescript
+// Only checks for explicit logout flags
+// Doesn't detect session invalidation
+const logoutParam = searchParams.get('logout');
+const fromLogout = sessionStorage.getItem('just_logged_out');
+```
+
+### Issue 3: Session Invalidation Detection (`app/api/auth/options.ts:248-256`)
+```typescript
+// Detects session invalidation
+// But doesn't set logout flags
+// Sign-in page doesn't know session was invalidated
+if (refreshedToken.error === "SessionNotActive") {
+ return {
+ ...refreshedToken,
+ accessToken: undefined,
+ // Should set a flag here to prevent auto-login
+ };
+}
+```
+
+## Summary
+
+**Why you're auto-logged in after iframe logout:**
+
+1. Iframe logs out directly (not via postMessage)
+2. Keycloak session is cleared
+3. Dashboard detects session invalidation
+4. User becomes "unauthenticated"
+5. Sign-in page auto-login logic triggers (after 1 second)
+6. Keycloak still has SSO session cookie
+7. User is auto-authenticated without credentials
+
+**The fix requires:**
+- Detecting session invalidation and setting logout flags
+- Preventing auto-login when session was invalidated
+- Only auto-login for truly new users (never logged in)
+
diff --git a/INACTIVITY_AND_LOGOUT_ANALYSIS.md b/INACTIVITY_AND_LOGOUT_ANALYSIS.md
new file mode 100644
index 00000000..7b73402b
--- /dev/null
+++ b/INACTIVITY_AND_LOGOUT_ANALYSIS.md
@@ -0,0 +1,232 @@
+# Inactivity Timeout and Logout Analysis
+
+## Issue 1: Dashboard Should Disconnect After 30 Minutes of Inactivity
+
+### Current State
+
+**Session Configuration** (`app/api/auth/options.ts:190`):
+```typescript
+session: {
+ strategy: "jwt",
+ maxAge: 30 * 24 * 60 * 60, // 30 days
+}
+```
+
+**SessionProvider Configuration** (`components/providers.tsx`):
+```typescript
+
+ {children}
+
+```
+
+### Problem Analysis
+
+1. **No Inactivity Detection**:
+ - NextAuth session is set to 30 days maximum
+ - No client-side inactivity timeout logic exists
+ - No activity tracking (mouse movements, clicks, keyboard input)
+ - SessionProvider doesn't have `refetchInterval` configured
+
+2. **How NextAuth Sessions Work**:
+ - NextAuth sessions are JWT-based (stateless)
+ - Session validity is checked on each request to `/api/auth/session`
+ - No automatic expiration based on inactivity
+ - Session expires only when `maxAge` is reached (30 days in your case)
+
+3. **What's Missing**:
+ - Client-side activity monitoring
+ - Automatic session invalidation after inactivity period
+ - Session refresh based on activity (not just time)
+
+### Root Cause
+
+**NextAuth doesn't track user activity** - it only tracks session age. The session will remain valid for 30 days regardless of whether the user is active or not.
+
+### Solution Requirements
+
+To implement 30-minute inactivity timeout, you need:
+
+1. **Client-Side Activity Tracking**:
+ - Monitor user activity (mouse, keyboard, clicks)
+ - Track last activity timestamp
+ - Store in `sessionStorage` or `localStorage`
+
+2. **Session Invalidation Logic**:
+ - Check inactivity period on each page interaction
+ - Call `signOut()` if inactivity exceeds 30 minutes
+ - Clear NextAuth session and Keycloak session
+
+3. **Activity Reset on User Actions**:
+ - Reset inactivity timer on any user interaction
+ - Update last activity timestamp
+
+4. **SessionProvider Configuration**:
+ - Optionally configure `refetchInterval` to check session periodically
+ - But this won't help with inactivity - it only refreshes the session
+
+### Implementation Approach
+
+The inactivity timeout must be implemented **client-side** because:
+- NextAuth sessions are stateless (JWT)
+- Server doesn't know about user activity
+- Activity tracking requires browser events
+
+**Recommended Implementation**:
+1. Create an `InactivityHandler` component
+2. Monitor user activity events (mousemove, keydown, click, scroll)
+3. Store last activity time in `sessionStorage`
+4. Check inactivity every minute (or on page focus)
+5. If inactivity > 30 minutes, trigger logout
+
+---
+
+## Issue 2: Applications Outside Dashboard Still Connected After Logout
+
+### Current Implementation
+
+**Logout Flow** (`components/main-nav.tsx`, `components/auth/signout-handler.tsx`):
+1. Clear NextAuth cookies
+2. Clear Keycloak cookies (client-side attempt)
+3. Call `/api/auth/end-sso-session` (NEW)
+ - Uses Keycloak Admin API: `adminClient.users.logout({ id: userId })`
+4. Sign out from NextAuth
+5. Redirect to Keycloak logout endpoint with `id_token_hint`
+
+### Problem Analysis
+
+**Why Applications Are Still Connected:**
+
+1. **Keycloak Admin API `users.logout()` Behavior**:
+ - The method `adminClient.users.logout({ id: userId })` **logs out the user from all client sessions**
+ - However, it may **NOT clear the SSO session cookie** (`KEYCLOAK_SESSION`)
+ - The SSO session cookie is what allows applications to auto-authenticate
+
+2. **SSO Session vs Client Sessions**:
+ - **Client Sessions**: Per OAuth client (dashboard, app1, app2, etc.)
+ - **SSO Session**: Realm-wide, shared across all clients
+ - `users.logout()` clears client sessions but may leave SSO session active
+ - Applications check for SSO session cookie, not client sessions
+
+3. **Cookie Domain/Path Issues**:
+ - Keycloak cookies are set on Keycloak's domain
+ - Client-side `clearKeycloakCookies()` may not work if:
+ - Cookies are `HttpOnly` (can't be cleared from JavaScript)
+ - Cookies are on different domain (cross-domain restrictions)
+ - Cookies have different path/domain settings
+
+4. **Logout Endpoint Behavior**:
+ - Keycloak logout endpoint (`/protocol/openid-connect/logout`) with `id_token_hint`:
+ - Clears the **client session** for that specific OAuth client
+ - May clear SSO session **only if it's the last client session**
+ - If other applications have active sessions, SSO session persists
+
+### Root Cause
+
+**The SSO session cookie persists** because:
+1. `users.logout()` Admin API method clears client sessions but may not clear SSO session cookie
+2. Keycloak logout endpoint only clears SSO session if it's the last client session
+3. If other applications have active sessions, the SSO session remains valid
+4. Applications check for SSO session cookie, not client sessions
+
+### Why This Happens
+
+**Keycloak's SSO Design**:
+- SSO session is designed to persist across client logouts
+- This allows users to stay logged in across multiple applications
+- Logging out from one application shouldn't log out from all applications
+- This is **by design** for SSO functionality
+
+**However**, when you want **global logout**, you need to:
+1. Clear the SSO session cookie explicitly
+2. Or ensure all client sessions are logged out first
+3. Or use Keycloak's Single Logout (SLO) feature
+
+### Solution Requirements
+
+To ensure applications are logged out:
+
+1. **Keycloak Configuration** (Server-Side):
+ - Enable **Front-Channel Logout** for all clients
+ - Configure **Back-Channel Logout URLs** for each client
+ - This allows Keycloak to notify all applications when logout occurs
+
+2. **Admin API Limitations**:
+ - `users.logout()` may not clear SSO session cookie
+ - Need to use Keycloak's logout endpoint with proper parameters
+ - Or use Keycloak Admin API to end SSO session directly (if available)
+
+3. **Alternative Approach**:
+ - Use Keycloak's **Single Logout (SLO)** feature
+ - Configure all clients to participate in SLO
+ - When one client logs out, all clients are notified
+
+### What's Actually Happening
+
+When you call `/api/auth/end-sso-session`:
+1. ✅ Admin API `users.logout()` is called
+2. ✅ All client sessions are logged out
+3. ❌ SSO session cookie may still exist
+4. ❌ Applications check SSO session cookie → still authenticated
+
+When you redirect to Keycloak logout endpoint:
+1. ✅ Dashboard client session is cleared
+2. ✅ If it's the last client session, SSO session is cleared
+3. ❌ If other applications have active sessions, SSO session persists
+4. ❌ Applications can still authenticate using SSO session cookie
+
+### Verification Steps
+
+To verify why applications are still connected:
+
+1. **Check if Admin API call succeeds**:
+ - Look for console logs: "Successfully ended SSO session for user: {userId}"
+ - Check for errors in `/api/auth/end-sso-session` endpoint
+
+2. **Check Keycloak session cookies**:
+ - After logout, check browser cookies for:
+ - `KEYCLOAK_SESSION`
+ - `KEYCLOAK_SESSION_LEGACY`
+ - `KEYCLOAK_IDENTITY`
+ - If these cookies still exist, SSO session is still active
+
+3. **Check if other applications have active sessions**:
+ - If other applications are open in other tabs/windows
+ - They may have active client sessions
+ - This prevents SSO session from being cleared
+
+4. **Check Keycloak Admin Console**:
+ - Navigate to: Users → [User] → Sessions
+ - Check if sessions are actually cleared
+ - Verify SSO session status
+
+### Recommended Solutions
+
+**Option 1: Keycloak Configuration (Recommended)**
+- Enable Front-Channel Logout for all clients
+- Configure Back-Channel Logout URLs
+- This ensures all applications are notified of logout
+
+**Option 2: Clear SSO Session Cookie Explicitly**
+- After Admin API logout, redirect to Keycloak logout endpoint
+- Use `kc_action=LOGOUT` parameter (already implemented)
+- Ensure all client sessions are logged out first
+
+**Option 3: Use Keycloak Single Logout (SLO)**
+- Configure all clients to participate in SLO
+- When dashboard logs out, all clients are automatically logged out
+- Requires Keycloak configuration changes
+
+---
+
+## Summary
+
+### Issue 1: 30-Minute Inactivity Timeout
+- **Status**: Not implemented
+- **Reason**: NextAuth doesn't track activity, only session age
+- **Solution**: Client-side activity tracking + automatic logout
+
+### Issue 2: Applications Still Connected
+- **Status**: Partially working
+- **Reason**: SSO session cookie persists even after client sessions are cleared
+- **Solution**: Keycloak configuration (Front-Channel Logout) or SLO
+
diff --git a/app/api/auth/options.ts b/app/api/auth/options.ts
index 703ef698..4499af43 100644
--- a/app/api/auth/options.ts
+++ b/app/api/auth/options.ts
@@ -280,8 +280,11 @@ export const authOptions: NextAuthOptions = {
hasAccessToken: !!token.accessToken,
hasRefreshToken: !!token.refreshToken
});
+
// Return null to make NextAuth treat user as unauthenticated
// This will trigger automatic redirect to sign-in page
+ // The client-side code will detect session invalidation by checking for
+ // session cookie existence when status is unauthenticated
return null as any;
}
diff --git a/app/signin/page.tsx b/app/signin/page.tsx
index 3b243ae6..94626407 100644
--- a/app/signin/page.tsx
+++ b/app/signin/page.tsx
@@ -13,14 +13,26 @@ export default function SignIn() {
const isLogoutRedirect = useRef(false);
// Check if this is a logout redirect (from Keycloak post_logout_redirect_uri)
+ // OR if session was invalidated (e.g., from iframe logout)
useEffect(() => {
// Check URL parameters or session storage for logout flag
const logoutParam = searchParams.get('logout');
const fromLogout = sessionStorage.getItem('just_logged_out');
+ const sessionInvalidated = sessionStorage.getItem('session_invalidated');
- if (logoutParam === 'true' || fromLogout === 'true') {
+ // Check if there's a NextAuth session cookie that's now invalid
+ // This indicates the session was invalidated (e.g., by iframe logout)
+ const hasInvalidSessionCookie = document.cookie
+ .split(';')
+ .some(c => c.trim().startsWith('next-auth.session-token=') ||
+ c.trim().startsWith('__Secure-next-auth.session-token=') ||
+ c.trim().startsWith('__Host-next-auth.session-token='));
+
+ // If session was invalidated or this is a logout redirect, prevent auto-login
+ if (logoutParam === 'true' || fromLogout === 'true' || sessionInvalidated === 'true') {
isLogoutRedirect.current = true;
sessionStorage.removeItem('just_logged_out');
+ sessionStorage.removeItem('session_invalidated');
// Clear any OAuth parameters from URL to prevent callback processing
const url = new URL(window.location.href);
@@ -42,7 +54,17 @@ export default function SignIn() {
// Don't auto-trigger login after logout
return;
}
- }, [searchParams]);
+
+ // Detect session invalidation: if status becomes unauthenticated
+ // and we had a session cookie, it means the session was invalidated
+ if (status === 'unauthenticated' && hasInvalidSessionCookie && !hasAttemptedLogin.current) {
+ console.log('Session invalidation detected (likely from iframe logout), preventing auto-login');
+ sessionStorage.setItem('session_invalidated', 'true');
+ isLogoutRedirect.current = true;
+ // Don't auto-login - user must manually click "Se connecter"
+ return;
+ }
+ }, [searchParams, status]);
useEffect(() => {
// If user is already authenticated, redirect to home
@@ -51,8 +73,9 @@ export default function SignIn() {
return;
}
- // Don't auto-login if this is a logout redirect or we've already attempted login
- if (isLogoutRedirect.current || hasAttemptedLogin.current) {
+ // Don't auto-login if this is a logout redirect, session was invalidated, or we've already attempted login
+ const sessionInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
+ if (isLogoutRedirect.current || sessionInvalidated || hasAttemptedLogin.current) {
return;
}
@@ -62,14 +85,36 @@ export default function SignIn() {
}
// Only trigger Keycloak sign-in if not authenticated, not loading, and not from logout
- // Add a longer delay to ensure OAuth callbacks have completed
+ // AND only if this is a truly new user (never logged in), not a session invalidation
if (status === "unauthenticated") {
+ // Check if there's evidence of a previous session (session cookie exists but invalid)
+ // This indicates session was invalidated, not a new user
+ const hasSessionCookie = document.cookie
+ .split(';')
+ .some(c => {
+ const cookie = c.trim();
+ return cookie.startsWith('next-auth.session-token=') ||
+ cookie.startsWith('__Secure-next-auth.session-token=') ||
+ cookie.startsWith('__Host-next-auth.session-token=');
+ });
+
+ // If there's a session cookie but status is unauthenticated, session was invalidated
+ // Don't auto-login in this case
+ if (hasSessionCookie) {
+ console.log('Session cookie detected but status is unauthenticated - session was invalidated, preventing auto-login');
+ sessionStorage.setItem('session_invalidated', 'true');
+ isLogoutRedirect.current = true;
+ return;
+ }
+
+ // Only auto-login for truly new users (no session cookie)
hasAttemptedLogin.current = true;
// Longer delay to ensure we're not in a logout redirect flow or OAuth callback
const timer = setTimeout(() => {
// Double-check we're still unauthenticated and not in a logout flow
- if (!isLogoutRedirect.current) {
- // Trigger Keycloak sign-in
+ const stillInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
+ if (!isLogoutRedirect.current && !stillInvalidated) {
+ // Trigger Keycloak sign-in only for new users
signIn("keycloak", { callbackUrl: "/" });
}
}, 1000);
@@ -112,8 +157,11 @@ export default function SignIn() {
}
}, [session]);
- // Show logout message if coming from logout
- const showLogoutMessage = isLogoutRedirect.current || searchParams.get('logout') === 'true';
+ // Show logout message if coming from logout or session was invalidated
+ const sessionInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
+ const showLogoutMessage = isLogoutRedirect.current ||
+ searchParams.get('logout') === 'true' ||
+ sessionInvalidated;
return (
{showLogoutMessage
- ? "Vous avez été déconnecté avec succès"
+ ? (sessionInvalidated
+ ? "Votre session a expiré. Veuillez vous reconnecter."
+ : "Vous avez été déconnecté avec succès")
: initializationStatus === "initializing"
? "Initialisation de votre espace..."
: initializationStatus === "success"
@@ -142,8 +192,11 @@ export default function SignIn() {