diff --git a/app/loggedout/page.tsx b/app/loggedout/page.tsx
index a042a7ec..a6aa2ad4 100644
--- a/app/loggedout/page.tsx
+++ b/app/loggedout/page.tsx
@@ -1,21 +1,64 @@
"use client";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { clearAuthCookies } from "@/lib/session";
import Link from "next/link";
export default function LoggedOut() {
+ const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
+
// Clear auth cookies again on this page as an extra precaution
useEffect(() => {
- // Run an additional cookie cleanup on this page
- clearAuthCookies();
+ const checkAndClearSessions = async () => {
+ try {
+ // Additional browser storage clearing
+ console.log('Performing complete browser storage cleanup');
+
+ // Clear cookies
+ clearAuthCookies();
+
+ // Clear session storage
+ try {
+ sessionStorage.clear();
+ console.log('Session storage cleared');
+ } catch (e) {
+ console.error('Error clearing session storage:', e);
+ }
+
+ // Clear local storage items related to auth
+ try {
+ const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'oidc', 'user'];
+
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key) {
+ const keyLower = key.toLowerCase();
+ if (authLocalStoragePrefixes.some(prefix => keyLower.includes(prefix))) {
+ console.log(`Clearing localStorage: ${key}`);
+ localStorage.removeItem(key);
+ }
+ }
+ }
+ console.log('Local storage auth items cleared');
+ } catch (e) {
+ console.error('Error clearing localStorage:', e);
+ }
+
+ // Double check for Keycloak specific cookies
+ const keycloakCookies = ['KEYCLOAK_SESSION', 'KEYCLOAK_IDENTITY', 'KC_RESTART'];
+ for (const cookieName of keycloakCookies) {
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=None; Secure;`;
+ document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
+ }
+
+ setSessionStatus('cleared');
+ } catch (error) {
+ console.error('Error during session cleanup:', error);
+ setSessionStatus('error');
+ }
+ };
- // Also clear session storage
- try {
- sessionStorage.clear();
- } catch (e) {
- console.error('Error clearing session storage:', e);
- }
+ checkAndClearSessions();
}, []);
return (
@@ -33,15 +76,38 @@ export default function LoggedOut() {
You have been logged out
-
- Your session has been successfully terminated and all authentication data has been cleared.
-
-
- Sign In Again
-
+
+ {sessionStatus === 'checking' && (
+
+ Verifying all sessions are terminated...
+
+ )}
+
+ {sessionStatus === 'cleared' && (
+
+ Your session has been completely terminated and all authentication data has been cleared.
+
+ )}
+
+ {sessionStatus === 'error' && (
+
+ Your session has been terminated, but there might be some residual session data.
+ For complete security, please close your browser.
+
+ )}
+
+
+
+ Sign In Again
+
+
+
+ Note: If you're automatically signed in again, try clearing your browser cookies or restarting your browser.
+
+
diff --git a/components/auth/signout-handler.tsx b/components/auth/signout-handler.tsx
index 03c45adf..b80239c3 100644
--- a/components/auth/signout-handler.tsx
+++ b/components/auth/signout-handler.tsx
@@ -13,17 +13,21 @@ export function SignOutHandler() {
// First, clear all auth-related cookies to ensure we break any local sessions
clearAuthCookies();
- // Create a temporary HTML form for direct POST logout (more reliable than redirect)
- if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER && session?.accessToken) {
- console.log('Directly calling Keycloak logout endpoint');
+ // Get Keycloak logout URL
+ if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
+ console.log('Preparing complete Keycloak logout');
- // Create a hidden form for POST logout
+ // Create a proper Keycloak logout URL with all required parameters for front-channel logout
+ const keycloakBaseUrl = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
+ const logoutEndpoint = `${keycloakBaseUrl}/protocol/openid-connect/logout`;
+
+ // Create form for POST logout (more reliable)
const form = document.createElement('form');
form.method = 'POST';
- form.action = `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER}/protocol/openid-connect/logout`;
+ form.action = logoutEndpoint;
- // Add the id_token_hint
- if (session.accessToken) {
+ // Add id_token_hint if available
+ if (session?.accessToken) {
const tokenInput = document.createElement('input');
tokenInput.type = 'hidden';
tokenInput.name = 'id_token_hint';
@@ -31,24 +35,37 @@ export function SignOutHandler() {
form.appendChild(tokenInput);
}
- // Add post_logout_redirect_uri pointing to a special loggedout page
+ // Add client_id parameter - CRITICAL for proper logout
+ const clientIdInput = document.createElement('input');
+ clientIdInput.type = 'hidden';
+ clientIdInput.name = 'client_id';
+ clientIdInput.value = process.env.NEXT_PUBLIC_KEYCLOAK_CLIENT_ID || 'lab';
+ form.appendChild(clientIdInput);
+
+ // Add post_logout_redirect_uri pointing to our logged out page
const redirectInput = document.createElement('input');
redirectInput.type = 'hidden';
redirectInput.name = 'post_logout_redirect_uri';
redirectInput.value = `${window.location.origin}/loggedout`;
form.appendChild(redirectInput);
+ // Add logout_hint=server to explicitly request server-side session cleanup
+ const logoutHintInput = document.createElement('input');
+ logoutHintInput.type = 'hidden';
+ logoutHintInput.name = 'logout_hint';
+ logoutHintInput.value = 'server';
+ form.appendChild(logoutHintInput);
+
// Append to body and submit
document.body.appendChild(form);
+ console.log('Submitting Keycloak logout form with server-side logout');
form.submit();
} else {
console.log('No Keycloak configuration found, performing simple redirect');
- // Fallback if no Keycloak config or session
window.location.href = '/loggedout';
}
} catch (error) {
console.error('Error during logout:', error);
- // Fallback if something goes wrong
window.location.href = '/loggedout';
}
};
@@ -65,7 +82,7 @@ export function SignOutHandler() {
Logging out...
-
Please wait while we sign you out.
+
Please wait while we sign you out completely.
);
diff --git a/lib/session.ts b/lib/session.ts
index 0bc8eeaa..53931e4c 100644
--- a/lib/session.ts
+++ b/lib/session.ts
@@ -94,7 +94,7 @@ export function clearAuthCookies() {
const cookies = document.cookie.split(';');
console.log('Clearing all auth cookies');
- // List of known auth-related cookie prefixes
+ // List of known auth-related cookie prefixes and specific cookies
const authCookiePrefixes = [
'next-auth.',
'__Secure-next-auth.',
@@ -105,9 +105,25 @@ export function clearAuthCookies() {
'OAuth_Token_Request_State',
'OAUTH2_CLIENT_ID',
'OAUTH2_STATE',
- 'XSRF-TOKEN'
+ 'XSRF-TOKEN',
+ 'AUTH_SESSION_',
+ 'identity',
+ 'session',
+ 'connect.sid'
];
+ // Specific Keycloak cookies that need to be cleared
+ const specificCookies = [
+ 'KEYCLOAK_SESSION',
+ 'KEYCLOAK_IDENTITY',
+ 'KEYCLOAK_REMEMBER_ME',
+ 'KC_RESTART',
+ 'KEYCLOAK_SESSION_LEGACY',
+ 'KEYCLOAK_IDENTITY_LEGACY'
+ ];
+
+ console.log(`Processing ${cookies.length} cookies`);
+
for (const cookie of cookies) {
const [name] = cookie.split('=');
const trimmedName = name.trim();
@@ -115,34 +131,49 @@ export function clearAuthCookies() {
// Check if this is an auth-related cookie
const isAuthCookie = authCookiePrefixes.some(prefix =>
trimmedName.startsWith(prefix)
- );
+ ) || specificCookies.includes(trimmedName);
// Also clear cookies with auth-related terms
const containsAuthTerm =
trimmedName.toLowerCase().includes('auth') ||
trimmedName.toLowerCase().includes('token') ||
- trimmedName.toLowerCase().includes('session');
+ trimmedName.toLowerCase().includes('session') ||
+ trimmedName.toLowerCase().includes('login') ||
+ trimmedName.toLowerCase().includes('id');
if (isAuthCookie || containsAuthTerm) {
console.log(`Clearing cookie: ${trimmedName}`);
- // Clear the cookie with various domain/path combinations
- // Standard path
- document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
+ // Try different combinations to ensure the cookie is cleared
+ const paths = ['/', '/auth', '/realms', '/admin'];
+ const domains = [
+ window.location.hostname, // Exact domain
+ `.${window.location.hostname}`, // Domain with leading dot
+ window.location.hostname.split('.').slice(-2).join('.') // Root domain
+ ];
- // Root domain
- const domain = window.location.hostname.split('.').slice(-2).join('.');
- document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain};`;
-
- // Full domain
- document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname};`;
+ // Try each combination of path and domain
+ for (const path of paths) {
+ // Try without domain
+ document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path};`;
+
+ // Try with SameSite and Secure attributes
+ document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; SameSite=None; Secure;`;
+
+ // Try with different domains
+ for (const domain of domains) {
+ document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain};`;
+ document.cookie = `${trimmedName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${domain}; SameSite=None; Secure;`;
+ }
+ }
}
}
// Clear localStorage items that might be related to authentication
try {
- const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'user'];
+ const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'user', 'oidc', 'login'];
+ console.log(`Checking localStorage (${localStorage.length} items)`);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key) {
@@ -156,4 +187,33 @@ export function clearAuthCookies() {
} catch (e) {
console.error('Error clearing localStorage:', e);
}
+
+ // Also try to clear sessionStorage
+ try {
+ console.log('Clearing sessionStorage');
+ sessionStorage.clear();
+ } catch (e) {
+ console.error('Error clearing sessionStorage:', e);
+ }
+
+ // Check for any IndexedDB databases related to auth
+ try {
+ if (window.indexedDB) {
+ window.indexedDB.databases().then(databases => {
+ databases.forEach(db => {
+ if (db.name &&
+ (db.name.includes('auth') ||
+ db.name.includes('keycloak') ||
+ db.name.includes('token'))) {
+ console.log(`Deleting IndexedDB database: ${db.name}`);
+ window.indexedDB.deleteDatabase(db.name);
+ }
+ });
+ }).catch(err => {
+ console.error('Error accessing IndexedDB databases:', err);
+ });
+ }
+ } catch (e) {
+ console.error('Error clearing IndexedDB:', e);
+ }
}
\ No newline at end of file