auth flow

This commit is contained in:
alma 2025-05-02 11:08:02 +02:00
parent ecd587fd8c
commit d882325da2
3 changed files with 186 additions and 43 deletions

View File

@ -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() {
<h2 className="text-3xl font-bold text-white mb-4">
You have been logged out
</h2>
<p className="text-white/80 mb-8">
Your session has been successfully terminated and all authentication data has been cleared.
</p>
<Link
href="/signin"
className="inline-block px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors"
>
Sign In Again
</Link>
{sessionStatus === 'checking' && (
<p className="text-white/80 mb-4">
Verifying all sessions are terminated...
</p>
)}
{sessionStatus === 'cleared' && (
<p className="text-white/80 mb-4">
Your session has been completely terminated and all authentication data has been cleared.
</p>
)}
{sessionStatus === 'error' && (
<p className="text-white/80 mb-4">
Your session has been terminated, but there might be some residual session data.
For complete security, please close your browser.
</p>
)}
<div className="mt-6">
<Link
href="/signin"
className="inline-block px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors mb-4"
>
Sign In Again
</Link>
<p className="text-white/60 text-sm mt-4">
Note: If you're automatically signed in again, try clearing your browser cookies or restarting your browser.
</p>
</div>
</div>
</div>
</div>

View File

@ -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() {
<div className="w-full h-full flex items-center justify-center">
<div className="text-center">
<h2 className="text-2xl font-bold">Logging out...</h2>
<p className="text-gray-500 mt-2">Please wait while we sign you out.</p>
<p className="text-gray-500 mt-2">Please wait while we sign you out completely.</p>
</div>
</div>
);

View File

@ -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);
}
}