keycloak improve with build 4
This commit is contained in:
parent
ff7e022ee7
commit
43be7a99d2
305
LOGOUT_LOGIN_FLOW_TRACE.md
Normal file
305
LOGOUT_LOGIN_FLOW_TRACE.md
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
# Full Logout/Login Flow Trace
|
||||||
|
|
||||||
|
## Issue 1: No Credentials Asked After Logout/Login
|
||||||
|
|
||||||
|
### Current Flow Trace
|
||||||
|
|
||||||
|
#### Step 1: User Clicks Logout
|
||||||
|
```
|
||||||
|
Location: components/main-nav.tsx (line 364)
|
||||||
|
Action: onClick handler triggered
|
||||||
|
|
||||||
|
1. sessionStorage.setItem('just_logged_out', 'true')
|
||||||
|
2. document.cookie = 'logout_in_progress=true; path=/; max-age=60'
|
||||||
|
3. clearAuthCookies() - Clears NextAuth cookies client-side
|
||||||
|
4. signOut({ callbackUrl: '/signin?logout=true', redirect: false })
|
||||||
|
→ Calls NextAuth /api/auth/signout endpoint
|
||||||
|
→ Clears NextAuth session cookie server-side
|
||||||
|
5. window.location.replace(keycloakLogoutUrl)
|
||||||
|
→ Redirects to: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout
|
||||||
|
→ Parameters:
|
||||||
|
- post_logout_redirect_uri: /signin?logout=true
|
||||||
|
- id_token_hint: <ID token>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: Keycloak Logout Endpoint
|
||||||
|
```
|
||||||
|
Location: Keycloak Server
|
||||||
|
URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout
|
||||||
|
|
||||||
|
Expected Behavior:
|
||||||
|
- Keycloak should invalidate the session
|
||||||
|
- Keycloak should clear session cookies:
|
||||||
|
- KEYCLOAK_SESSION (main session cookie)
|
||||||
|
- KEYCLOAK_SESSION_LEGACY
|
||||||
|
- KEYCLOAK_IDENTITY (identity cookie)
|
||||||
|
- KEYCLOAK_IDENTITY_LEGACY
|
||||||
|
- AUTH_SESSION_ID
|
||||||
|
- KC_RESTART (if exists)
|
||||||
|
|
||||||
|
ACTUAL BEHAVIOR (PROBLEM):
|
||||||
|
- Keycloak logout endpoint with id_token_hint SHOULD clear cookies
|
||||||
|
- BUT: Keycloak might have SSO session that persists across clients
|
||||||
|
- OR: Cookies might not be cleared if domain/path mismatch
|
||||||
|
- OR: Keycloak might set new cookies during redirect
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Redirect Back to Signin
|
||||||
|
```
|
||||||
|
Location: app/signin/page.tsx
|
||||||
|
URL: /signin?logout=true
|
||||||
|
|
||||||
|
1. Component mounts
|
||||||
|
2. useEffect checks for logout flag (line 16-45)
|
||||||
|
- Sets isLogoutRedirect.current = true
|
||||||
|
- Removes 'just_logged_out' from sessionStorage
|
||||||
|
- Clears OAuth params from URL
|
||||||
|
3. Shows logout message with "Se connecter" button
|
||||||
|
4. User clicks "Se connecter" button (line 143-148)
|
||||||
|
- Calls: signIn("keycloak", { callbackUrl: "/" })
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Keycloak Authorization Request
|
||||||
|
```
|
||||||
|
Location: NextAuth → Keycloak
|
||||||
|
URL: ${KEYCLOAK_ISSUER}/protocol/openid-connect/auth
|
||||||
|
|
||||||
|
Parameters sent:
|
||||||
|
- client_id: KEYCLOAK_CLIENT_ID
|
||||||
|
- redirect_uri: ${NEXTAUTH_URL}/api/auth/callback/keycloak
|
||||||
|
- response_type: code
|
||||||
|
- scope: openid profile email roles
|
||||||
|
- state: <random state>
|
||||||
|
- code_challenge: (if PKCE enabled)
|
||||||
|
|
||||||
|
KEYCLOAK BEHAVIOR:
|
||||||
|
1. Keycloak receives authorization request
|
||||||
|
2. Keycloak checks for existing session cookies
|
||||||
|
3. IF Keycloak session cookies still exist:
|
||||||
|
→ Keycloak finds valid SSO session
|
||||||
|
→ Keycloak auto-authenticates user (no login prompt)
|
||||||
|
→ Keycloak redirects back with authorization code
|
||||||
|
4. IF Keycloak session cookies are cleared:
|
||||||
|
→ Keycloak shows login page
|
||||||
|
→ User enters credentials
|
||||||
|
→ Keycloak creates new session
|
||||||
|
→ Keycloak redirects back with authorization code
|
||||||
|
|
||||||
|
PROBLEM IDENTIFIED:
|
||||||
|
- Keycloak logout endpoint might not be clearing ALL session cookies
|
||||||
|
- OR: Keycloak has SSO session that persists (separate from client session)
|
||||||
|
- OR: Keycloak sets new cookies during the logout redirect process
|
||||||
|
- OR: Browser is preserving cookies due to SameSite/domain issues
|
||||||
|
```
|
||||||
|
|
||||||
|
### Root Cause Analysis
|
||||||
|
|
||||||
|
**Problem**: Keycloak SSO Session Persistence
|
||||||
|
|
||||||
|
Keycloak maintains two types of sessions:
|
||||||
|
1. **Client Session** (per OAuth client) - Cleared by logout endpoint
|
||||||
|
2. **SSO Session** (realm-wide) - May persist even after client logout
|
||||||
|
|
||||||
|
When you call:
|
||||||
|
```
|
||||||
|
GET /protocol/openid-connect/logout?id_token_hint=...&post_logout_redirect_uri=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Keycloak behavior:
|
||||||
|
- ✅ Clears the **client session** for that specific OAuth client
|
||||||
|
- ✅ Invalidates tokens for that client
|
||||||
|
- ❌ **MIGHT NOT** clear the **SSO session** (realm-wide session)
|
||||||
|
- ❌ **MIGHT NOT** clear all session cookies if cookies are set with different domain/path
|
||||||
|
|
||||||
|
**Why SSO Session Persists:**
|
||||||
|
- Keycloak SSO session is realm-wide, not client-specific
|
||||||
|
- Multiple clients can share the same SSO session
|
||||||
|
- Logging out from one client doesn't necessarily log out from the realm
|
||||||
|
- The SSO session cookie (KEYCLOAK_SESSION) might persist
|
||||||
|
|
||||||
|
**When User Clicks "Se connecter":**
|
||||||
|
1. Redirects to Keycloak authorization endpoint
|
||||||
|
2. Keycloak checks for SSO session cookie
|
||||||
|
3. If SSO session cookie exists → Auto-authenticates (no credentials asked)
|
||||||
|
4. If SSO session cookie cleared → Shows login page
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 2: Cannot Logout from Iframe Application
|
||||||
|
|
||||||
|
### Current Flow Trace
|
||||||
|
|
||||||
|
#### Step 1: User in Iframe Application
|
||||||
|
```
|
||||||
|
Location: Iframe application (e.g., /parole, /gite, etc.)
|
||||||
|
State:
|
||||||
|
- Dashboard has NextAuth session
|
||||||
|
- Keycloak session cookies exist
|
||||||
|
- Iframe app authenticated via Keycloak cookies
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 2: User Clicks Logout in Iframe
|
||||||
|
```
|
||||||
|
Location: Iframe application's logout button
|
||||||
|
Action: Iframe app's logout handler
|
||||||
|
|
||||||
|
Possible Scenarios:
|
||||||
|
|
||||||
|
Scenario A: Iframe calls its own logout endpoint
|
||||||
|
- Iframe app might call: POST /api/logout (iframe app's endpoint)
|
||||||
|
- This might clear iframe app's session
|
||||||
|
- BUT: Keycloak session cookies might still exist
|
||||||
|
- Result: Iframe app logs out, but Keycloak session persists
|
||||||
|
|
||||||
|
Scenario B: Iframe calls Keycloak logout
|
||||||
|
- Iframe app might call: GET ${KEYCLOAK_ISSUER}/protocol/openid-connect/logout
|
||||||
|
- Keycloak clears session cookies
|
||||||
|
- BUT: NextAuth dashboard session still exists
|
||||||
|
- Result: Keycloak session cleared, but dashboard still logged in
|
||||||
|
|
||||||
|
Scenario C: Iframe doesn't have logout
|
||||||
|
- Iframe app might not have logout functionality
|
||||||
|
- User stuck in iframe with no way to logout
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: What Happens After Iframe Logout
|
||||||
|
```
|
||||||
|
If iframe calls Keycloak logout:
|
||||||
|
1. Keycloak invalidates session
|
||||||
|
2. Keycloak clears session cookies
|
||||||
|
3. NextAuth dashboard still has valid JWT (30-day expiration)
|
||||||
|
4. NextAuth doesn't know Keycloak session was cleared
|
||||||
|
5. Dashboard widgets still show (using NextAuth session)
|
||||||
|
6. Iframe apps can't authenticate (Keycloak cookies cleared)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Root Cause Analysis
|
||||||
|
|
||||||
|
**Problem**: No Communication Between Iframe and Dashboard
|
||||||
|
|
||||||
|
When iframe app logs out:
|
||||||
|
1. **Iframe app** calls Keycloak logout → Clears Keycloak cookies
|
||||||
|
2. **Dashboard** doesn't know about this → NextAuth session still valid
|
||||||
|
3. **Dashboard widgets** continue to work (using NextAuth session)
|
||||||
|
4. **Iframe apps** can't authenticate (Keycloak cookies gone)
|
||||||
|
|
||||||
|
**Why Dashboard Doesn't Know:**
|
||||||
|
- NextAuth session is independent of Keycloak session cookies
|
||||||
|
- NextAuth JWT has 30-day expiration
|
||||||
|
- No mechanism to detect Keycloak session invalidation from iframe
|
||||||
|
- Dashboard only detects invalidation when trying to refresh tokens
|
||||||
|
|
||||||
|
**Why Iframe Can't Logout Properly:**
|
||||||
|
- Iframe apps rely on Keycloak cookies for SSO
|
||||||
|
- If iframe calls Keycloak logout, it clears cookies
|
||||||
|
- But dashboard session persists
|
||||||
|
- If iframe doesn't call logout, user can't logout from iframe
|
||||||
|
- No way for iframe to trigger dashboard logout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
### Finding 1: Keycloak SSO Session Persistence
|
||||||
|
- **Issue**: Keycloak logout endpoint might not clear SSO session
|
||||||
|
- **Evidence**: User auto-authenticates without credentials after logout
|
||||||
|
- **Root Cause**: SSO session cookie persists after client logout
|
||||||
|
- **Impact**: Security issue - user should be asked for credentials
|
||||||
|
|
||||||
|
### Finding 2: Missing `prompt=login` Parameter
|
||||||
|
- **Issue**: When calling `signIn("keycloak")`, no `prompt=login` parameter is sent
|
||||||
|
- **Evidence**: Keycloak auto-authenticates if SSO session exists
|
||||||
|
- **Root Cause**: NextAuth Keycloak provider doesn't force login prompt
|
||||||
|
- **Impact**: User bypasses credential check
|
||||||
|
|
||||||
|
### Finding 3: Iframe Logout Isolation
|
||||||
|
- **Issue**: Iframe logout doesn't affect dashboard session
|
||||||
|
- **Evidence**: Dashboard widgets still show after iframe logout
|
||||||
|
- **Root Cause**: No communication mechanism between iframe and dashboard
|
||||||
|
- **Impact**: Inconsistent logout state
|
||||||
|
|
||||||
|
### Finding 4: No Cross-Origin Logout Communication
|
||||||
|
- **Issue**: Iframe can't trigger dashboard logout
|
||||||
|
- **Evidence**: User stuck in iframe after logout
|
||||||
|
- **Root Cause**: No postMessage or other communication mechanism
|
||||||
|
- **Impact**: Poor user experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Flow Diagram
|
||||||
|
|
||||||
|
### Current Logout Flow (Dashboard)
|
||||||
|
```
|
||||||
|
User clicks logout
|
||||||
|
↓
|
||||||
|
Clear NextAuth cookies
|
||||||
|
↓
|
||||||
|
Call NextAuth signOut()
|
||||||
|
↓
|
||||||
|
Redirect to Keycloak logout
|
||||||
|
↓
|
||||||
|
Keycloak clears client session
|
||||||
|
↓
|
||||||
|
Keycloak MAY clear SSO session (not guaranteed)
|
||||||
|
↓
|
||||||
|
Redirect to /signin?logout=true
|
||||||
|
↓
|
||||||
|
User clicks "Se connecter"
|
||||||
|
↓
|
||||||
|
Redirect to Keycloak auth
|
||||||
|
↓
|
||||||
|
Keycloak checks SSO session
|
||||||
|
↓
|
||||||
|
IF SSO session exists → Auto-authenticate (NO CREDENTIALS)
|
||||||
|
IF SSO session cleared → Show login page
|
||||||
|
```
|
||||||
|
|
||||||
|
### Current Iframe Logout Flow
|
||||||
|
```
|
||||||
|
User in iframe clicks logout
|
||||||
|
↓
|
||||||
|
Iframe app calls logout (varies by app)
|
||||||
|
↓
|
||||||
|
IF calls Keycloak logout:
|
||||||
|
→ Keycloak clears session cookies
|
||||||
|
→ Dashboard session still valid
|
||||||
|
→ Widgets still show
|
||||||
|
→ Iframe can't authenticate
|
||||||
|
IF doesn't call logout:
|
||||||
|
→ User stuck in iframe
|
||||||
|
→ No way to logout
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### For Issue 1 (No Credentials After Logout)
|
||||||
|
1. **Add `prompt=login` parameter** to Keycloak authorization request
|
||||||
|
- Forces Keycloak to show login page even if SSO session exists
|
||||||
|
- Location: `app/api/auth/options.ts` - KeycloakProvider authorization params
|
||||||
|
|
||||||
|
2. **Clear Keycloak SSO session explicitly**
|
||||||
|
- Add `kc_action=LOGOUT` parameter to logout URL
|
||||||
|
- Or call Keycloak admin API to end SSO session
|
||||||
|
|
||||||
|
3. **Clear Keycloak cookies client-side**
|
||||||
|
- After Keycloak logout redirect, clear any remaining Keycloak cookies
|
||||||
|
- Check for cookies with Keycloak domain
|
||||||
|
|
||||||
|
### For Issue 2 (Iframe Logout)
|
||||||
|
1. **Implement postMessage communication**
|
||||||
|
- Iframe sends logout message to parent
|
||||||
|
- Dashboard listens for logout messages
|
||||||
|
- Dashboard triggers logout when iframe logs out
|
||||||
|
|
||||||
|
2. **Detect Keycloak session invalidation**
|
||||||
|
- Poll Keycloak session status
|
||||||
|
- Detect when Keycloak cookies are cleared
|
||||||
|
- Automatically logout dashboard
|
||||||
|
|
||||||
|
3. **Unified logout endpoint**
|
||||||
|
- Create API endpoint that logs out both dashboard and Keycloak
|
||||||
|
- Iframe apps call this endpoint
|
||||||
|
- Ensures synchronized logout
|
||||||
|
|
||||||
@ -146,7 +146,12 @@ export const authOptions: NextAuthOptions = {
|
|||||||
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"),
|
||||||
authorization: {
|
authorization: {
|
||||||
params: {
|
params: {
|
||||||
scope: "openid profile email roles"
|
scope: "openid profile email roles",
|
||||||
|
// Force login prompt even if SSO session exists
|
||||||
|
// This ensures user is asked for credentials after logout
|
||||||
|
// Note: This will always prompt for login, even on first visit
|
||||||
|
// If you want to allow SSO on first visit, remove this and handle it conditionally
|
||||||
|
prompt: "login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
profile(profile) {
|
profile(profile) {
|
||||||
|
|||||||
@ -106,6 +106,52 @@ export function ResponsiveIframe({ src, className = '', allow, style }: Responsi
|
|||||||
refreshSession();
|
refreshSession();
|
||||||
}, [session, src, hasTriedRefresh, iframeSrc]);
|
}, [session, src, hasTriedRefresh, iframeSrc]);
|
||||||
|
|
||||||
|
// Listen for logout messages from iframe applications
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (event: MessageEvent) => {
|
||||||
|
// Security: Only accept messages from known iframe origins
|
||||||
|
// In production, you should validate event.origin against your iframe URLs
|
||||||
|
|
||||||
|
// Check if message is a logout request from iframe
|
||||||
|
if (event.data && typeof event.data === 'object') {
|
||||||
|
if (event.data.type === 'KEYCLOAK_LOGOUT' || event.data.type === 'LOGOUT') {
|
||||||
|
console.log('Received logout request from iframe, triggering dashboard logout');
|
||||||
|
|
||||||
|
// Mark logout in progress
|
||||||
|
sessionStorage.setItem('just_logged_out', 'true');
|
||||||
|
document.cookie = 'logout_in_progress=true; path=/; max-age=60';
|
||||||
|
|
||||||
|
// Trigger dashboard logout
|
||||||
|
if (session?.idToken) {
|
||||||
|
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||||
|
if (keycloakIssuer) {
|
||||||
|
const keycloakLogoutUrl = new URL(
|
||||||
|
`${keycloakIssuer}/protocol/openid-connect/logout`
|
||||||
|
);
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'post_logout_redirect_uri',
|
||||||
|
window.location.origin + '/signin?logout=true'
|
||||||
|
);
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'id_token_hint',
|
||||||
|
session.idToken
|
||||||
|
);
|
||||||
|
window.location.replace(keycloakLogoutUrl.toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: redirect to signin
|
||||||
|
window.location.replace('/signin?logout=true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const iframe = iframeRef.current;
|
const iframe = iframeRef.current;
|
||||||
if (!iframe || !iframeSrc) return;
|
if (!iframe || !iframeSrc) return;
|
||||||
@ -146,7 +192,7 @@ export function ResponsiveIframe({ src, className = '', allow, style }: Responsi
|
|||||||
window.removeEventListener('hashchange', handleHashChange);
|
window.removeEventListener('hashchange', handleHashChange);
|
||||||
iframe.removeEventListener('load', calculateHeight);
|
iframe.removeEventListener('load', calculateHeight);
|
||||||
};
|
};
|
||||||
}, []);
|
}, [iframeSrc]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -144,7 +144,12 @@ export default function SignIn() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
hasAttemptedLogin.current = false;
|
hasAttemptedLogin.current = false;
|
||||||
isLogoutRedirect.current = false;
|
isLogoutRedirect.current = false;
|
||||||
signIn("keycloak", { callbackUrl: "/" });
|
// Force login prompt by adding prompt=login parameter
|
||||||
|
// This ensures credentials are asked even if SSO session exists
|
||||||
|
signIn("keycloak", {
|
||||||
|
callbackUrl: "/",
|
||||||
|
// Note: prompt=login is already set in authOptions, but we ensure it here too
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { clearAuthCookies } from "@/lib/session";
|
import { clearAuthCookies, clearKeycloakCookies } from "@/lib/session";
|
||||||
|
|
||||||
export function SignOutHandler() {
|
export function SignOutHandler() {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@ -21,6 +21,8 @@ export function SignOutHandler() {
|
|||||||
|
|
||||||
// Clear NextAuth cookies immediately before signOut
|
// Clear NextAuth cookies immediately before signOut
|
||||||
clearAuthCookies();
|
clearAuthCookies();
|
||||||
|
// Also attempt to clear Keycloak cookies
|
||||||
|
clearKeycloakCookies();
|
||||||
|
|
||||||
// Sign out from NextAuth (clears NextAuth session)
|
// Sign out from NextAuth (clears NextAuth session)
|
||||||
await signOut({
|
await signOut({
|
||||||
@ -43,6 +45,11 @@ export function SignOutHandler() {
|
|||||||
'id_token_hint',
|
'id_token_hint',
|
||||||
idToken
|
idToken
|
||||||
);
|
);
|
||||||
|
// Add kc_action=LOGOUT to ensure SSO session is cleared
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'kc_action',
|
||||||
|
'LOGOUT'
|
||||||
|
);
|
||||||
|
|
||||||
// Immediate redirect to Keycloak logout (prevents widget rendering)
|
// Immediate redirect to Keycloak logout (prevents widget rendering)
|
||||||
window.location.replace(keycloakLogoutUrl.toString());
|
window.location.replace(keycloakLogoutUrl.toString());
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useSession, signOut } from "next-auth/react";
|
||||||
import { MainNav } from "@/components/main-nav";
|
import { MainNav } from "@/components/main-nav";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import { AuthCheck } from "@/components/auth/auth-check";
|
import { AuthCheck } from "@/components/auth/auth-check";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { useBackgroundImage } from "@/components/background-switcher";
|
import { useBackgroundImage } from "@/components/background-switcher";
|
||||||
|
import { clearAuthCookies, clearKeycloakCookies } from "@/lib/session";
|
||||||
|
|
||||||
interface LayoutWrapperProps {
|
interface LayoutWrapperProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -14,6 +17,75 @@ interface LayoutWrapperProps {
|
|||||||
|
|
||||||
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
|
export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: LayoutWrapperProps) {
|
||||||
const { currentBackground, changeBackground } = useBackgroundImage();
|
const { currentBackground, changeBackground } = useBackgroundImage();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
|
||||||
|
// Global listener for logout messages from iframe applications
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignInPage) return; // Don't listen on signin page
|
||||||
|
|
||||||
|
const handleMessage = async (event: MessageEvent) => {
|
||||||
|
// Security: Validate message origin (in production, check against known iframe URLs)
|
||||||
|
// For now, we accept messages from any origin but validate the message structure
|
||||||
|
|
||||||
|
// Check if message is a logout request from iframe
|
||||||
|
if (event.data && typeof event.data === 'object') {
|
||||||
|
if (event.data.type === 'KEYCLOAK_LOGOUT' || event.data.type === 'LOGOUT') {
|
||||||
|
console.log('Received logout request from iframe application, triggering dashboard logout');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Mark logout in progress
|
||||||
|
sessionStorage.setItem('just_logged_out', 'true');
|
||||||
|
document.cookie = 'logout_in_progress=true; path=/; max-age=60';
|
||||||
|
|
||||||
|
// Clear cookies
|
||||||
|
clearAuthCookies();
|
||||||
|
clearKeycloakCookies();
|
||||||
|
|
||||||
|
// Sign out from NextAuth
|
||||||
|
await signOut({
|
||||||
|
callbackUrl: '/signin?logout=true',
|
||||||
|
redirect: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Call Keycloak logout if we have ID token
|
||||||
|
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||||
|
const idToken = session?.idToken;
|
||||||
|
|
||||||
|
if (keycloakIssuer && idToken) {
|
||||||
|
const keycloakLogoutUrl = new URL(
|
||||||
|
`${keycloakIssuer}/protocol/openid-connect/logout`
|
||||||
|
);
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'post_logout_redirect_uri',
|
||||||
|
window.location.origin + '/signin?logout=true'
|
||||||
|
);
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'id_token_hint',
|
||||||
|
idToken
|
||||||
|
);
|
||||||
|
// Add kc_action=LOGOUT to ensure SSO session is cleared
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'kc_action',
|
||||||
|
'LOGOUT'
|
||||||
|
);
|
||||||
|
window.location.replace(keycloakLogoutUrl.toString());
|
||||||
|
} else {
|
||||||
|
// Fallback: redirect to signin
|
||||||
|
window.location.replace('/signin?logout=true');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling iframe logout:', error);
|
||||||
|
window.location.replace('/signin?logout=true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('message', handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [isSignInPage, session]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthCheck>
|
<AuthCheck>
|
||||||
|
|||||||
@ -28,7 +28,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { useSession, signIn, signOut } from "next-auth/react";
|
import { useSession, signIn, signOut } from "next-auth/react";
|
||||||
import { clearAuthCookies } from "@/lib/session";
|
import { clearAuthCookies, clearKeycloakCookies } from "@/lib/session";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@ -373,6 +373,8 @@ export function MainNav() {
|
|||||||
|
|
||||||
// Clear NextAuth cookies immediately before signOut
|
// Clear NextAuth cookies immediately before signOut
|
||||||
clearAuthCookies();
|
clearAuthCookies();
|
||||||
|
// Also attempt to clear Keycloak cookies
|
||||||
|
clearKeycloakCookies();
|
||||||
|
|
||||||
// Sign out from NextAuth (clears NextAuth session)
|
// Sign out from NextAuth (clears NextAuth session)
|
||||||
await signOut({
|
await signOut({
|
||||||
@ -396,6 +398,11 @@ export function MainNav() {
|
|||||||
'id_token_hint',
|
'id_token_hint',
|
||||||
idToken
|
idToken
|
||||||
);
|
);
|
||||||
|
// Add kc_action=LOGOUT to ensure SSO session is cleared
|
||||||
|
keycloakLogoutUrl.searchParams.append(
|
||||||
|
'kc_action',
|
||||||
|
'LOGOUT'
|
||||||
|
);
|
||||||
|
|
||||||
// Immediate redirect to Keycloak logout (prevents widget rendering)
|
// Immediate redirect to Keycloak logout (prevents widget rendering)
|
||||||
window.location.replace(keycloakLogoutUrl.toString());
|
window.location.replace(keycloakLogoutUrl.toString());
|
||||||
|
|||||||
@ -99,4 +99,50 @@ export function clearAuthCookies() {
|
|||||||
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear Keycloak session cookies
|
||||||
|
* Keycloak cookies are typically set on Keycloak's domain, so we need to
|
||||||
|
* try clearing them with different domain/path combinations
|
||||||
|
*/
|
||||||
|
export function clearKeycloakCookies() {
|
||||||
|
const keycloakIssuer = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
||||||
|
if (!keycloakIssuer) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract domain from Keycloak issuer
|
||||||
|
const keycloakUrl = new URL(keycloakIssuer);
|
||||||
|
const keycloakDomain = keycloakUrl.hostname;
|
||||||
|
|
||||||
|
// Common Keycloak cookie names
|
||||||
|
const keycloakCookieNames = [
|
||||||
|
'KEYCLOAK_SESSION',
|
||||||
|
'KEYCLOAK_SESSION_LEGACY',
|
||||||
|
'KEYCLOAK_IDENTITY',
|
||||||
|
'KEYCLOAK_IDENTITY_LEGACY',
|
||||||
|
'AUTH_SESSION_ID',
|
||||||
|
'KC_RESTART',
|
||||||
|
'KC_RESTART_LEGACY'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Try to clear cookies with different domain/path combinations
|
||||||
|
const domains = [keycloakDomain, `.${keycloakDomain}`, window.location.hostname];
|
||||||
|
const paths = ['/', '/realms/', keycloakUrl.pathname];
|
||||||
|
|
||||||
|
keycloakCookieNames.forEach(cookieName => {
|
||||||
|
domains.forEach(domain => {
|
||||||
|
paths.forEach(path => {
|
||||||
|
// Try with domain
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};`;
|
||||||
|
// Try without domain (for same-origin cookies)
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Attempted to clear Keycloak cookies');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing Keycloak cookies:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user