From d882325da2cbe62fbfa09d221f6b9e9920934aeb Mon Sep 17 00:00:00 2001 From: alma Date: Fri, 2 May 2025 11:08:02 +0200 Subject: [PATCH] auth flow --- app/loggedout/page.tsx | 102 +++++++++++++++++++++++----- components/auth/signout-handler.tsx | 39 ++++++++--- lib/session.ts | 88 ++++++++++++++++++++---- 3 files changed, 186 insertions(+), 43 deletions(-) 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