keycloak improve with build 6
This commit is contained in:
parent
f0c109ed8e
commit
61bc7d6809
259
IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md
Normal file
259
IFRAME_LOGOUT_AUTO_LOGIN_ANALYSIS.md
Normal file
@ -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)
|
||||||
|
|
||||||
232
INACTIVITY_AND_LOGOUT_ANALYSIS.md
Normal file
232
INACTIVITY_AND_LOGOUT_ANALYSIS.md
Normal file
@ -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
|
||||||
|
<SessionProvider>
|
||||||
|
{children}
|
||||||
|
</SessionProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
@ -280,8 +280,11 @@ export const authOptions: NextAuthOptions = {
|
|||||||
hasAccessToken: !!token.accessToken,
|
hasAccessToken: !!token.accessToken,
|
||||||
hasRefreshToken: !!token.refreshToken
|
hasRefreshToken: !!token.refreshToken
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return null to make NextAuth treat user as unauthenticated
|
// Return null to make NextAuth treat user as unauthenticated
|
||||||
// This will trigger automatic redirect to sign-in page
|
// 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;
|
return null as any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,14 +13,26 @@ export default function SignIn() {
|
|||||||
const isLogoutRedirect = useRef(false);
|
const isLogoutRedirect = useRef(false);
|
||||||
|
|
||||||
// Check if this is a logout redirect (from Keycloak post_logout_redirect_uri)
|
// Check if this is a logout redirect (from Keycloak post_logout_redirect_uri)
|
||||||
|
// OR if session was invalidated (e.g., from iframe logout)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check URL parameters or session storage for logout flag
|
// Check URL parameters or session storage for logout flag
|
||||||
const logoutParam = searchParams.get('logout');
|
const logoutParam = searchParams.get('logout');
|
||||||
const fromLogout = sessionStorage.getItem('just_logged_out');
|
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;
|
isLogoutRedirect.current = true;
|
||||||
sessionStorage.removeItem('just_logged_out');
|
sessionStorage.removeItem('just_logged_out');
|
||||||
|
sessionStorage.removeItem('session_invalidated');
|
||||||
|
|
||||||
// Clear any OAuth parameters from URL to prevent callback processing
|
// Clear any OAuth parameters from URL to prevent callback processing
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
@ -42,7 +54,17 @@ export default function SignIn() {
|
|||||||
// Don't auto-trigger login after logout
|
// Don't auto-trigger login after logout
|
||||||
return;
|
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(() => {
|
useEffect(() => {
|
||||||
// If user is already authenticated, redirect to home
|
// If user is already authenticated, redirect to home
|
||||||
@ -51,8 +73,9 @@ export default function SignIn() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't auto-login if this is a logout redirect or we've already attempted login
|
// Don't auto-login if this is a logout redirect, session was invalidated, or we've already attempted login
|
||||||
if (isLogoutRedirect.current || hasAttemptedLogin.current) {
|
const sessionInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
|
||||||
|
if (isLogoutRedirect.current || sessionInvalidated || hasAttemptedLogin.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,14 +85,36 @@ export default function SignIn() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only trigger Keycloak sign-in if not authenticated, not loading, and not from logout
|
// 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") {
|
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;
|
hasAttemptedLogin.current = true;
|
||||||
// Longer delay to ensure we're not in a logout redirect flow or OAuth callback
|
// Longer delay to ensure we're not in a logout redirect flow or OAuth callback
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
// Double-check we're still unauthenticated and not in a logout flow
|
// Double-check we're still unauthenticated and not in a logout flow
|
||||||
if (!isLogoutRedirect.current) {
|
const stillInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
|
||||||
// Trigger Keycloak sign-in
|
if (!isLogoutRedirect.current && !stillInvalidated) {
|
||||||
|
// Trigger Keycloak sign-in only for new users
|
||||||
signIn("keycloak", { callbackUrl: "/" });
|
signIn("keycloak", { callbackUrl: "/" });
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@ -112,8 +157,11 @@ export default function SignIn() {
|
|||||||
}
|
}
|
||||||
}, [session]);
|
}, [session]);
|
||||||
|
|
||||||
// Show logout message if coming from logout
|
// Show logout message if coming from logout or session was invalidated
|
||||||
const showLogoutMessage = isLogoutRedirect.current || searchParams.get('logout') === 'true';
|
const sessionInvalidated = sessionStorage.getItem('session_invalidated') === 'true';
|
||||||
|
const showLogoutMessage = isLogoutRedirect.current ||
|
||||||
|
searchParams.get('logout') === 'true' ||
|
||||||
|
sessionInvalidated;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -129,7 +177,9 @@ export default function SignIn() {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||||
{showLogoutMessage
|
{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"
|
: initializationStatus === "initializing"
|
||||||
? "Initialisation de votre espace..."
|
? "Initialisation de votre espace..."
|
||||||
: initializationStatus === "success"
|
: initializationStatus === "success"
|
||||||
@ -142,8 +192,11 @@ export default function SignIn() {
|
|||||||
<div className="mt-4 text-center">
|
<div className="mt-4 text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
// Clear all logout/invalidation flags before logging in
|
||||||
hasAttemptedLogin.current = false;
|
hasAttemptedLogin.current = false;
|
||||||
isLogoutRedirect.current = false;
|
isLogoutRedirect.current = false;
|
||||||
|
sessionStorage.removeItem('just_logged_out');
|
||||||
|
sessionStorage.removeItem('session_invalidated');
|
||||||
// Force login prompt by adding prompt=login parameter
|
// Force login prompt by adding prompt=login parameter
|
||||||
// This ensures credentials are asked even if SSO session exists
|
// This ensures credentials are asked even if SSO session exists
|
||||||
signIn("keycloak", {
|
signIn("keycloak", {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user