auth flow
This commit is contained in:
parent
ed199d7a00
commit
fa05961404
15
.env
15
.env
@ -27,7 +27,7 @@ NEXT_PUBLIC_IFRAME_LEARN_URL=https://apprendre.slm-lab.net
|
|||||||
NEXT_PUBLIC_IFRAME_PAROLE_URL=https://parole.slm-lab.net/channel/City
|
NEXT_PUBLIC_IFRAME_PAROLE_URL=https://parole.slm-lab.net/channel/City
|
||||||
NEXT_PUBLIC_IFRAME_CHAPTER_URL=https://chapitre.slm-lab.net
|
NEXT_PUBLIC_IFRAME_CHAPTER_URL=https://chapitre.slm-lab.net
|
||||||
NEXT_PUBLIC_IFRAME_AGILITY_URL=https://agilite.slm-lab.net/oidc/login
|
NEXT_PUBLIC_IFRAME_AGILITY_URL=https://agilite.slm-lab.net/oidc/login
|
||||||
NEXT_PUBLIC_IFRAME_ARTLAB_URL=https://artlab.slm-lab.net
|
NEXT_PUBLIC_IFRAME_DESIGN_URL=https://artlab.slm-lab.net
|
||||||
NEXT_PUBLIC_IFRAME_GITE_URL=https://gite.slm-lab.net/user/oauth2/cube
|
NEXT_PUBLIC_IFRAME_GITE_URL=https://gite.slm-lab.net/user/oauth2/cube
|
||||||
NEXT_PUBLIC_IFRAME_CALCULATION_URL=https://calcul.slm-lab.net
|
NEXT_PUBLIC_IFRAME_CALCULATION_URL=https://calcul.slm-lab.net
|
||||||
NEXT_PUBLIC_IFRAME_MEDIATIONS_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?client_id=mediations.slm-lab.net&redirect_uri=https%3A%2F%2Fmediations.slm-lab.net%2F%3Fopenid_mode%3Dtrue&scope=openid%20profile%20email&response_type=code
|
NEXT_PUBLIC_IFRAME_MEDIATIONS_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?client_id=mediations.slm-lab.net&redirect_uri=https%3A%2F%2Fmediations.slm-lab.net%2F%3Fopenid_mode%3Dtrue&scope=openid%20profile%20email&response_type=code
|
||||||
@ -45,7 +45,7 @@ NEXT_PUBLIC_IFRAME_ANNOUNCEMENT_URL=https://espace.slm-lab.net/apps/announcement
|
|||||||
NEXT_PUBLIC_IFRAME_HEALTHVIEW_URL=https://espace.slm-lab.net/apps/health/?embedMode=true&hideNavigation=true
|
NEXT_PUBLIC_IFRAME_HEALTHVIEW_URL=https://espace.slm-lab.net/apps/health/?embedMode=true&hideNavigation=true
|
||||||
NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?response_type=code&scope=openid&client_id=page.slm-lab.net&state=f72528f6756bc132e76dd258691b71cf&redirect_uri=https%3A%2F%2Fpage.slm-lab.net%2Fwp-admin%2F
|
NEXT_PUBLIC_IFRAME_MISSIONVIEW_URL=https://connect.slm-lab.net/realms/cercle/protocol/openid-connect/auth?response_type=code&scope=openid&client_id=page.slm-lab.net&state=f72528f6756bc132e76dd258691b71cf&redirect_uri=https%3A%2F%2Fpage.slm-lab.net%2Fwp-admin%2F
|
||||||
NEXT_PUBLIC_IFRAME_USERSVIEW_URL=https://example.com/users-view
|
NEXT_PUBLIC_IFRAME_USERSVIEW_URL=https://example.com/users-view
|
||||||
NEXT_PUBLIC_IFRAME_THEMESSAGE_URL=https://lemessage.slm-lab.net/admin/
|
NEXT_PUBLIC_IFRAME_THEMESSAGE_URL=https://lemessage.slm-lab.net/auth/oidc
|
||||||
NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL=https://alma.slm-lab.net
|
NEXT_PUBLIC_IFRAME_AI_ASSISTANT_URL=https://alma.slm-lab.net
|
||||||
|
|
||||||
ROCKET_CHAT_TOKEN=w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB
|
ROCKET_CHAT_TOKEN=w91TYgkH-Z67Oz72usYdkW5TZLLRwnre7qyAhp7aHJB
|
||||||
@ -85,3 +85,14 @@ MICROSOFT_CLIENT_ID="afaffea5-4e10-462a-aa64-e73baf642c57"
|
|||||||
MICROSOFT_CLIENT_SECRET="eIx8Q~N3ZnXTjTsVM3ECZio4G7t.BO6AYlD1-b2h"
|
MICROSOFT_CLIENT_SECRET="eIx8Q~N3ZnXTjTsVM3ECZio4G7t.BO6AYlD1-b2h"
|
||||||
MICROSOFT_REDIRECT_URI="https://lab.slm-lab.net/ms"
|
MICROSOFT_REDIRECT_URI="https://lab.slm-lab.net/ms"
|
||||||
MICROSOFT_TENANT_ID="cb4281a9-4a3e-4ff5-9a85-8425dd04e2b2"
|
MICROSOFT_TENANT_ID="cb4281a9-4a3e-4ff5-9a85-8425dd04e2b2"
|
||||||
|
|
||||||
|
|
||||||
|
# Reduce session size to prevent chunking
|
||||||
|
NEXTAUTH_JWT_CALLBACK_STORE_SESSION_DATA=false
|
||||||
|
NEXTAUTH_SESSION_STORE_SESSION_TOKEN=false
|
||||||
|
NEXTAUTH_JWT_STORE_RAW_TOKEN=false
|
||||||
|
# Cookie security and sharing
|
||||||
|
NEXTAUTH_COOKIE_DOMAIN=.slm-lab.net
|
||||||
|
NEXTAUTH_URL=https://lab.slm-lab.net
|
||||||
|
NEXTAUTH_COOKIE_SECURE=true
|
||||||
|
NEXTAUTH_COOKIE_SAMESITE=none
|
||||||
@ -1 +0,0 @@
|
|||||||
NEXT_PUBLIC_IFRAME_DESIGN_URL=https://design.slm-lab.net
|
|
||||||
35
app/api/auth/full-logout/route.ts
Normal file
35
app/api/auth/full-logout/route.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { getServerSession } from "next-auth";
|
||||||
|
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
try {
|
||||||
|
const session = await getServerSession(authOptions);
|
||||||
|
const userId = session?.user?.id;
|
||||||
|
const keycloakUrl = process.env.KEYCLOAK_ISSUER;
|
||||||
|
const clientId = process.env.KEYCLOAK_CLIENT_ID;
|
||||||
|
const redirectUri = req.nextUrl.searchParams.get('redirectUri') || process.env.NEXTAUTH_URL;
|
||||||
|
|
||||||
|
// Build Keycloak logout URL
|
||||||
|
let logoutUrl = `${keycloakUrl}/protocol/openid-connect/logout?client_id=${clientId}`;
|
||||||
|
|
||||||
|
if (redirectUri) {
|
||||||
|
logoutUrl += `&post_logout_redirect_uri=${encodeURIComponent(redirectUri)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Initiating full Keycloak logout for user ${userId || 'unknown'}`);
|
||||||
|
console.log(`Logout URL: ${logoutUrl}`);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
logoutUrl,
|
||||||
|
message: "Keycloak logout URL generated"
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating Keycloak logout URL:", error);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: false,
|
||||||
|
error: "Failed to generate Keycloak logout URL"
|
||||||
|
}, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,52 +1,85 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getServerSession } from 'next-auth/next';
|
import { getServerSession } from 'next-auth';
|
||||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||||
import { cleanupUserSessions } from '@/lib/redis';
|
import { getRedisClient } from '@/lib/redis';
|
||||||
import { closeUserImapConnections } from '@/lib/services/email-service';
|
import { closeUserImapConnections } from '@/lib/services/email-service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API endpoint to clean up user sessions and invalidate cached data
|
* API endpoint to clean up user sessions and invalidate cached data
|
||||||
* Called during logout to ensure proper cleanup of all connections
|
* Called during logout to ensure proper cleanup of all connections
|
||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
|
||||||
// Get the user ID either from the session or request body
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
const body = await request.json().catch(() => ({}));
|
const userId = session?.user?.id;
|
||||||
|
|
||||||
// Get user ID from session or from request body
|
|
||||||
const userId = session?.user?.id || body.userId;
|
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return NextResponse.json({
|
return NextResponse.json({ success: false, error: "No authenticated user found" }, { status: 401 });
|
||||||
success: false,
|
|
||||||
error: 'No user ID provided or user not authenticated'
|
|
||||||
}, { status: 400 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Processing session cleanup for user ${userId}`);
|
await cleanupUserSessions(userId, false);
|
||||||
|
|
||||||
// 1. Close any active IMAP connections using the dedicated function
|
return NextResponse.json({ success: true, userId, message: "Session cleaned up" });
|
||||||
const closedConnections = await closeUserImapConnections(userId);
|
}
|
||||||
|
|
||||||
// 2. Clean up Redis data
|
export async function POST(req: NextRequest) {
|
||||||
await cleanupUserSessions(userId);
|
try {
|
||||||
|
const body = await req.json();
|
||||||
|
const userId = body.userId;
|
||||||
|
const preserveSso = !!body.preserveSso;
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ success: false, error: "No user ID provided" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await cleanupUserSessions(userId, preserveSso);
|
||||||
|
|
||||||
// 3. Return success response with details
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Session cleanup completed for user ${userId}`,
|
userId,
|
||||||
details: {
|
preserveSso,
|
||||||
closedConnections,
|
message: `Session cleaned up${preserveSso ? ' (SSO preserved)' : ''}`
|
||||||
redisCleanupPerformed: true
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in session cleanup:', error);
|
console.error("Error in session-cleanup POST:", error);
|
||||||
return NextResponse.json({
|
return NextResponse.json({ success: false, error: "Failed to parse request" }, { status: 400 });
|
||||||
success: false,
|
}
|
||||||
error: 'Session cleanup failed',
|
}
|
||||||
details: error instanceof Error ? error.message : 'Unknown error'
|
|
||||||
}, { status: 500 });
|
async function cleanupUserSessions(userId: string, preserveSso: boolean) {
|
||||||
|
try {
|
||||||
|
const redis = await getRedisClient();
|
||||||
|
if (!redis) {
|
||||||
|
console.error("Redis client not available for session cleanup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cleaning up sessions for user ${userId}${preserveSso ? ' (preserving SSO)' : ''}`);
|
||||||
|
|
||||||
|
// Find all keys for this user
|
||||||
|
const userKeys = await redis.keys(`*:${userId}*`);
|
||||||
|
|
||||||
|
if (userKeys.length > 0) {
|
||||||
|
console.log(`Found ${userKeys.length} keys for user ${userId}:`, userKeys);
|
||||||
|
|
||||||
|
// If preserving SSO, only delete application-specific keys
|
||||||
|
const keysToDelete = preserveSso
|
||||||
|
? userKeys.filter(key => !key.includes('keycloak') && !key.includes('sso'))
|
||||||
|
: userKeys;
|
||||||
|
|
||||||
|
if (keysToDelete.length > 0) {
|
||||||
|
await redis.del(keysToDelete);
|
||||||
|
console.log(`Deleted ${keysToDelete.length} keys for user ${userId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`No application keys to delete while preserving SSO`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`No keys found for user ${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close any active IMAP connections
|
||||||
|
await closeUserImapConnections(userId);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error cleaning up sessions for user ${userId}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3,155 +3,180 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
interface ResponsiveIframeProps {
|
export interface ResponsiveIframeProps {
|
||||||
src: string;
|
src: string;
|
||||||
className?: string;
|
title?: string;
|
||||||
allow?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
token?: string;
|
token?: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
hideUntilLoad?: boolean;
|
||||||
|
allowFullScreen?: boolean;
|
||||||
|
scrolling?: boolean;
|
||||||
|
heightOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ResponsiveIframe({ src, className = '', allow, style, token }: ResponsiveIframeProps) {
|
export default function ResponsiveIframe({
|
||||||
|
src,
|
||||||
|
title = 'Embedded content',
|
||||||
|
token,
|
||||||
|
className = '',
|
||||||
|
style = {},
|
||||||
|
hideUntilLoad = false,
|
||||||
|
allowFullScreen = false,
|
||||||
|
scrolling = true,
|
||||||
|
heightOffset = 0,
|
||||||
|
}: ResponsiveIframeProps) {
|
||||||
|
const [height, setHeight] = useState<number>(0);
|
||||||
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
|
const [authError, setAuthError] = useState<boolean>(false);
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const silentAuthRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const silentAuthTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [authError, setAuthError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// Add token parameter only if token is provided
|
// Append token to src if provided
|
||||||
const fullSrc = token ?
|
const fullSrc = token ? `${src}${src.includes('?') ? '&' : '?'}token=${token}` : src;
|
||||||
`${src}${src.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` :
|
|
||||||
src;
|
|
||||||
|
|
||||||
// Handle silent authentication refresh
|
// Handle silent authentication refresh
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let silentRefreshTimer: NodeJS.Timeout;
|
// Setup silent authentication check every 15 minutes
|
||||||
|
const setupSilentAuth = () => {
|
||||||
// Set up periodic silent refresh (every 15 minutes)
|
if (silentAuthTimerRef.current) {
|
||||||
const startSilentRefresh = () => {
|
clearTimeout(silentAuthTimerRef.current);
|
||||||
silentRefreshTimer = setInterval(() => {
|
|
||||||
console.log('Performing silent authentication check for iframes');
|
|
||||||
|
|
||||||
// Create a hidden iframe for silent authentication
|
|
||||||
const refreshFrame = document.createElement('iframe');
|
|
||||||
refreshFrame.style.display = 'none';
|
|
||||||
refreshFrame.src = '/silent-refresh';
|
|
||||||
document.body.appendChild(refreshFrame);
|
|
||||||
|
|
||||||
// Remove iframe after it has loaded (5 seconds timeout)
|
|
||||||
setTimeout(() => {
|
|
||||||
if (refreshFrame && refreshFrame.parentNode) {
|
|
||||||
refreshFrame.parentNode.removeChild(refreshFrame);
|
|
||||||
}
|
}
|
||||||
}, 5000);
|
|
||||||
|
// Create a hidden iframe to check authentication status
|
||||||
|
silentAuthTimerRef.current = setTimeout(() => {
|
||||||
|
console.log('Running silent authentication check');
|
||||||
|
|
||||||
|
// Create the silent auth iframe if it doesn't exist
|
||||||
|
if (silentAuthRef.current && !silentAuthRef.current.src) {
|
||||||
|
silentAuthRef.current.src = '/silent-refresh';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup next check
|
||||||
|
setupSilentAuth();
|
||||||
}, 15 * 60 * 1000); // 15 minutes
|
}, 15 * 60 * 1000); // 15 minutes
|
||||||
};
|
};
|
||||||
|
|
||||||
if (session) {
|
// Handle messages from the silent auth iframe
|
||||||
startSilentRefresh();
|
const handleAuthMessage = (event: MessageEvent) => {
|
||||||
|
if (event.data && event.data.type === 'AUTH_STATUS') {
|
||||||
|
console.log('Received auth status:', event.data);
|
||||||
|
|
||||||
|
if (event.data.status === 'UNAUTHENTICATED') {
|
||||||
|
console.error('Silent authentication failed - user is not authenticated');
|
||||||
|
setAuthError(true);
|
||||||
|
|
||||||
|
// Force immediate refresh
|
||||||
|
if (silentAuthRef.current) {
|
||||||
|
silentAuthRef.current.src = '/silent-refresh';
|
||||||
}
|
}
|
||||||
|
} else if (event.data.status === 'AUTHENTICATED') {
|
||||||
return () => {
|
|
||||||
if (silentRefreshTimer) {
|
|
||||||
clearInterval(silentRefreshTimer);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [session]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const iframe = iframeRef.current;
|
|
||||||
if (!iframe) return;
|
|
||||||
|
|
||||||
const calculateHeight = () => {
|
|
||||||
const pageY = (elem: HTMLElement): number => {
|
|
||||||
return elem.offsetParent ?
|
|
||||||
(elem.offsetTop + pageY(elem.offsetParent as HTMLElement)) :
|
|
||||||
elem.offsetTop;
|
|
||||||
};
|
|
||||||
|
|
||||||
const height = document.documentElement.clientHeight;
|
|
||||||
const iframeY = pageY(iframe);
|
|
||||||
const newHeight = Math.max(0, height - iframeY);
|
|
||||||
iframe.style.height = `${newHeight}px`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleHashChange = () => {
|
|
||||||
if (window.location.hash && window.location.hash.length) {
|
|
||||||
const iframeURL = new URL(iframe.src);
|
|
||||||
iframeURL.hash = window.location.hash;
|
|
||||||
iframe.src = iframeURL.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle authentication messages from iframe
|
|
||||||
const handleMessage = (event: MessageEvent) => {
|
|
||||||
// Accept messages from our iframe or from silent auth iframe
|
|
||||||
if (event.source !== iframe.contentWindow &&
|
|
||||||
!event.data?.type?.startsWith('SILENT_AUTH_')) return;
|
|
||||||
|
|
||||||
const { type, data, error } = event.data || {};
|
|
||||||
|
|
||||||
// Handle auth-related messages
|
|
||||||
if (type === 'AUTH_ERROR' || type === 'SESSION_EXPIRED') {
|
|
||||||
console.log('Auth error in iframe:', data || error);
|
|
||||||
setAuthError(error || 'Authentication error');
|
|
||||||
} else if (type === 'SILENT_AUTH_SUCCESS') {
|
|
||||||
console.log('Silent authentication successful');
|
console.log('Silent authentication successful');
|
||||||
setAuthError(null);
|
setAuthError(false);
|
||||||
} else if (type === 'SILENT_AUTH_FAILURE') {
|
|
||||||
console.log('Silent authentication failed:', error);
|
|
||||||
// Only set error if it's persistent
|
|
||||||
if (error !== 'loading') {
|
|
||||||
setAuthError('Session expired');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial setup
|
window.addEventListener('message', handleAuthMessage);
|
||||||
calculateHeight();
|
setupSilentAuth();
|
||||||
handleHashChange();
|
|
||||||
|
|
||||||
// Event listeners
|
|
||||||
window.addEventListener('resize', calculateHeight);
|
|
||||||
window.addEventListener('hashchange', handleHashChange);
|
|
||||||
window.addEventListener('message', handleMessage);
|
|
||||||
iframe.addEventListener('load', calculateHeight);
|
|
||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('resize', calculateHeight);
|
window.removeEventListener('message', handleAuthMessage);
|
||||||
|
if (silentAuthTimerRef.current) {
|
||||||
|
clearTimeout(silentAuthTimerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Adjust iframe height based on window size
|
||||||
|
useEffect(() => {
|
||||||
|
const updateHeight = () => {
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const newHeight = viewportHeight - heightOffset;
|
||||||
|
setHeight(newHeight > 0 ? newHeight : 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateHeight();
|
||||||
|
window.addEventListener('resize', updateHeight);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateHeight);
|
||||||
|
};
|
||||||
|
}, [heightOffset]);
|
||||||
|
|
||||||
|
// Handle hash changes by updating iframe source
|
||||||
|
useEffect(() => {
|
||||||
|
const handleHashChange = () => {
|
||||||
|
const iframe = iframeRef.current;
|
||||||
|
if (iframe && iframe.src) {
|
||||||
|
const url = new URL(iframe.src);
|
||||||
|
|
||||||
|
// If there's a hash in the parent window's URL
|
||||||
|
if (window.location.hash) {
|
||||||
|
url.hash = window.location.hash;
|
||||||
|
iframe.src = url.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', handleHashChange);
|
||||||
|
return () => {
|
||||||
window.removeEventListener('hashchange', handleHashChange);
|
window.removeEventListener('hashchange', handleHashChange);
|
||||||
window.removeEventListener('message', handleMessage);
|
|
||||||
iframe.removeEventListener('load', calculateHeight);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Hidden iframe for silent authentication */}
|
||||||
|
<iframe
|
||||||
|
ref={silentAuthRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
title="Silent Authentication"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main content iframe */}
|
||||||
|
<div className="relative w-full h-full">
|
||||||
{authError && (
|
{authError && (
|
||||||
<div
|
<div className="absolute inset-0 flex items-center justify-center bg-red-50 bg-opacity-90 z-10">
|
||||||
style={{
|
<div className="text-center p-4">
|
||||||
backgroundColor: 'rgba(255, 0, 0, 0.1)',
|
<p className="text-red-600 font-semibold">Session expired or authentication error</p>
|
||||||
padding: '10px',
|
<button
|
||||||
borderRadius: '4px',
|
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
|
||||||
marginBottom: '10px'
|
onClick={() => window.location.href = '/api/auth/signin?callbackUrl=' + encodeURIComponent(window.location.href)}
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Authentication error: {authError}. The service might not work correctly.
|
Sign in again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<iframe
|
<iframe
|
||||||
ref={iframeRef}
|
ref={iframeRef}
|
||||||
id="myFrame"
|
|
||||||
src={fullSrc}
|
src={fullSrc}
|
||||||
className={`w-full border-none ${className}`}
|
title={title}
|
||||||
|
className={`w-full ${className}`}
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
height: height > 0 ? `${height}px` : '100%',
|
||||||
width: '100%',
|
border: 'none',
|
||||||
height: '100%',
|
visibility: hideUntilLoad && !loaded ? 'hidden' : 'visible',
|
||||||
...style
|
...style,
|
||||||
}}
|
}}
|
||||||
allow={allow}
|
onLoad={() => {
|
||||||
allowFullScreen
|
setLoaded(true);
|
||||||
|
}}
|
||||||
|
allowFullScreen={allowFullScreen}
|
||||||
|
scrolling={scrolling ? 'yes' : 'no'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{hideUntilLoad && !loaded && (
|
||||||
|
<div className="flex justify-center items-center w-full h-full absolute top-0 left-0">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,237 +1,280 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { clearAuthCookies } from "@/lib/session";
|
import { useSearchParams } from 'next/navigation';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default function LoggedOut() {
|
export default function LoggedOutPage() {
|
||||||
const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
|
const [sessionStatus, setSessionStatus] = useState<'checking' | 'cleared' | 'error'>('checking');
|
||||||
|
const [message, setMessage] = useState<string>('');
|
||||||
|
const [keycloakLogoutUrl, setKeycloakLogoutUrl] = useState<string | null>(null);
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
const forceLogout = new URLSearchParams(window.location.search).get('forceLogout') === 'true';
|
const searchParams = useSearchParams();
|
||||||
|
const preserveSso = searchParams.get('preserveSso') === 'true';
|
||||||
|
|
||||||
// Listen for any messages from iframes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const messageHandler = (event: MessageEvent) => {
|
// Listen for messages from iframes
|
||||||
// Handle any auth-related messages from iframes
|
const handleMessage = (event: MessageEvent) => {
|
||||||
if (event.data && event.data.type === 'AUTH_ERROR') {
|
if (event.data && event.data.type === 'AUTH_STATUS') {
|
||||||
console.log('Received auth error from iframe:', event.data);
|
console.log('Received auth status message:', event.data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', messageHandler);
|
window.addEventListener('message', handleMessage);
|
||||||
return () => window.removeEventListener('message', messageHandler);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Clear auth cookies again on this page as an extra precaution
|
const clearSessions = async () => {
|
||||||
useEffect(() => {
|
|
||||||
const checkAndClearSessions = async () => {
|
|
||||||
try {
|
try {
|
||||||
// Additional browser storage clearing
|
console.log(`Clearing sessions (preserveSso: ${preserveSso})`);
|
||||||
console.log('Performing complete browser storage cleanup');
|
|
||||||
|
|
||||||
// Add a hidden iframe to directly call Keycloak logout endpoint
|
// Try to get any stored user IDs for server-side cleanup
|
||||||
// This ensures the server-side Keycloak session is properly terminated
|
let userId = null;
|
||||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
|
||||||
console.log('Adding Keycloak logout iframe');
|
|
||||||
const keycloakBaseUrl = process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER;
|
|
||||||
const logoutEndpoint = `${keycloakBaseUrl}/protocol/openid-connect/logout`;
|
|
||||||
|
|
||||||
if (iframeRef.current) {
|
|
||||||
iframeRef.current.src = logoutEndpoint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to get any user ID from localStorage or sessionStorage for server-side cleanup
|
|
||||||
let userId = '';
|
|
||||||
try {
|
try {
|
||||||
// Check standard localStorage locations for userId
|
// Check localStorage first
|
||||||
const possibleUserIdKeys = [
|
userId = localStorage.getItem('userId') || sessionStorage.getItem('userId');
|
||||||
'userId',
|
|
||||||
'user_id',
|
|
||||||
'currentUser',
|
|
||||||
'user',
|
|
||||||
'keycloak.userId',
|
|
||||||
'auth.userId'
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const key of possibleUserIdKeys) {
|
// Try to get from sessionStorage as fallback
|
||||||
const value = localStorage.getItem(key) || sessionStorage.getItem(key);
|
if (!userId) {
|
||||||
if (value) {
|
const sessionData = sessionStorage.getItem('nextauth.session-token');
|
||||||
|
if (sessionData) {
|
||||||
try {
|
try {
|
||||||
// It might be a JSON object
|
const parsed = JSON.parse(atob(sessionData.split('.')[1]));
|
||||||
const parsed = JSON.parse(value);
|
userId = parsed.sub || parsed.id;
|
||||||
userId = parsed.id || parsed.userId || parsed.user_id || parsed.sub || '';
|
|
||||||
if (userId) break;
|
|
||||||
} catch {
|
|
||||||
// Or it might be a plain string
|
|
||||||
userId = value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Found user ID for server cleanup:', userId || 'None found');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error getting user ID from storage:', e);
|
console.error('Failed to parse session data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error accessing storage:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the server-side cleanup if we have a user ID
|
// If we found a user ID, call server-side cleanup
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
console.log(`Found user ID: ${userId}, cleaning up server-side`);
|
||||||
try {
|
try {
|
||||||
const cleanupResponse = await fetch('/api/auth/session-cleanup', {
|
const response = await fetch('/api/auth/session-cleanup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ userId }),
|
body: JSON.stringify({
|
||||||
|
userId,
|
||||||
|
preserveSso
|
||||||
|
}),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
const cleanupResult = await cleanupResponse.json();
|
const result = await response.json();
|
||||||
console.log('Server-side cleanup result:', cleanupResult);
|
console.log('Server cleanup result:', result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error calling server-side cleanup:', e);
|
console.error('Error during server-side cleanup:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear cookies
|
// If not preserving SSO, get Keycloak logout URL
|
||||||
clearAuthCookies();
|
if (!preserveSso) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/full-logout', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success && data.logoutUrl) {
|
||||||
|
setKeycloakLogoutUrl(data.logoutUrl);
|
||||||
|
console.log('Keycloak logout URL:', data.logoutUrl);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to get Keycloak logout URL:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear cookies appropriately based on preserve SSO setting
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
|
||||||
|
// Get all cookies names
|
||||||
|
const cookieNames = cookies.map(cookie => cookie.split('=')[0].trim());
|
||||||
|
|
||||||
|
// Find chunked cookies
|
||||||
|
const chunkedCookies = cookieNames
|
||||||
|
.filter(name => /next-auth.*\.\d+$/.test(name));
|
||||||
|
|
||||||
|
// Define cookies to clear
|
||||||
|
let cookiesToClear = [];
|
||||||
|
|
||||||
|
if (preserveSso) {
|
||||||
|
// Only clear app-specific cookies if preserving SSO
|
||||||
|
cookiesToClear = [
|
||||||
|
'next-auth.session-token',
|
||||||
|
'next-auth.csrf-token',
|
||||||
|
'next-auth.callback-url',
|
||||||
|
'__Secure-next-auth.session-token',
|
||||||
|
'__Host-next-auth.csrf-token',
|
||||||
|
...chunkedCookies
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
// Clear ALL auth-related cookies for full logout
|
||||||
|
const authCookies = cookieNames.filter(name =>
|
||||||
|
name.includes('auth') ||
|
||||||
|
name.includes('KEYCLOAK') ||
|
||||||
|
name.includes('KC_') ||
|
||||||
|
name.includes('session')
|
||||||
|
);
|
||||||
|
|
||||||
|
cookiesToClear = [
|
||||||
|
...authCookies,
|
||||||
|
...chunkedCookies,
|
||||||
|
'JSESSIONID',
|
||||||
|
'KEYCLOAK_SESSION',
|
||||||
|
'KEYCLOAK_IDENTITY',
|
||||||
|
'KC_RESTART'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the cookies with all possible path and domain combinations
|
||||||
|
const hostname = window.location.hostname;
|
||||||
|
const baseDomain = hostname.split('.').slice(-2).join('.');
|
||||||
|
|
||||||
|
cookiesToClear.forEach(cookieName => {
|
||||||
|
// Try various path and domain combinations to ensure complete cleanup
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${hostname};`;
|
||||||
|
|
||||||
|
// Only try this for multi-part domains
|
||||||
|
if (hostname !== baseDomain) {
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${baseDomain};`;
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${hostname};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add secure and SameSite attributes
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=None; Secure;`;
|
||||||
|
});
|
||||||
|
|
||||||
// Clear session storage
|
// Clear session storage
|
||||||
try {
|
try {
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
console.log('Session storage cleared');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error clearing session storage:', e);
|
console.error('Failed to clear sessionStorage:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear local storage items related to auth
|
// Clear auth-related localStorage items
|
||||||
try {
|
try {
|
||||||
const authLocalStoragePrefixes = ['token', 'auth', 'session', 'keycloak', 'kc', 'oidc', 'user', 'meteor'];
|
localStorage.removeItem('userId');
|
||||||
|
localStorage.removeItem('userName');
|
||||||
|
localStorage.removeItem('userEmail');
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
// Rocket Chat items
|
||||||
const key = localStorage.key(i);
|
localStorage.removeItem('Meteor.loginToken');
|
||||||
if (key) {
|
localStorage.removeItem('Meteor.userId');
|
||||||
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) {
|
} catch (e) {
|
||||||
console.error('Error clearing localStorage:', e);
|
console.error('Failed to clear localStorage items:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double check for Keycloak specific cookies and chunked cookies
|
// Notify parent window if we're in an iframe
|
||||||
const cookies = document.cookie.split(';');
|
if (window !== window.parent) {
|
||||||
const cookieNames = cookies.map(cookie => cookie.split('=')[0].trim());
|
|
||||||
|
|
||||||
// Look for chunked cookies
|
|
||||||
const chunkedCookies = cookieNames.filter(name => /\.\d+$/.test(name));
|
|
||||||
|
|
||||||
const keycloakCookies = [
|
|
||||||
'KEYCLOAK_SESSION',
|
|
||||||
'KEYCLOAK_IDENTITY',
|
|
||||||
'KC_RESTART',
|
|
||||||
'rc_token',
|
|
||||||
'rc_uid',
|
|
||||||
'Meteor.loginToken',
|
|
||||||
'AUTH_SESSION_ID',
|
|
||||||
'AUTH_SESSION_ID_LEGACY',
|
|
||||||
...chunkedCookies
|
|
||||||
];
|
|
||||||
|
|
||||||
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=/;`;
|
|
||||||
|
|
||||||
// Also try with root domain
|
|
||||||
const rootDomain = window.location.hostname.split('.').slice(-2).join('.');
|
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=.${rootDomain}; SameSite=None; Secure;`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additional paths that Keycloak might use
|
|
||||||
['/auth', '/realms'].forEach(path => {
|
|
||||||
keycloakCookies.forEach(cookieName => {
|
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=${path}; domain=${window.location.hostname}; SameSite=None; Secure;`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Notify any parent windows/iframes
|
|
||||||
try {
|
try {
|
||||||
if (window.parent && window.parent !== window) {
|
window.parent.postMessage({
|
||||||
window.parent.postMessage({ type: 'SESSION_CLEARED' }, '*');
|
type: 'AUTH_STATUS',
|
||||||
}
|
status: 'LOGGED_OUT',
|
||||||
|
preserveSso
|
||||||
|
}, '*');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error notifying parent window:', e);
|
console.error('Failed to send logout message to parent:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionStatus('cleared');
|
setSessionStatus('cleared');
|
||||||
|
setMessage(preserveSso
|
||||||
|
? 'You have been logged out of this application, but your SSO session is still active for other services.'
|
||||||
|
: 'You have been completely logged out of all services.'
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during session cleanup:', error);
|
console.error('Error during logout cleanup:', error);
|
||||||
setSessionStatus('error');
|
setSessionStatus('error');
|
||||||
|
setMessage('There was an error during the logout process.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAndClearSessions();
|
clearSessions();
|
||||||
}, []);
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('message', handleMessage);
|
||||||
|
};
|
||||||
|
}, [preserveSso]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 p-4">
|
||||||
className="min-h-screen flex items-center justify-center"
|
<div className="w-full max-w-md p-8 space-y-6 bg-white rounded-lg shadow-md">
|
||||||
style={{
|
|
||||||
backgroundImage: "url('/signin.jpg')",
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
backgroundRepeat: 'no-repeat'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Hidden iframe for direct Keycloak logout */}
|
|
||||||
<iframe
|
|
||||||
ref={iframeRef}
|
|
||||||
style={{ display: 'none' }}
|
|
||||||
title="keycloak-logout"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="w-full max-w-md p-8 bg-black/60 backdrop-blur-sm rounded-lg shadow-xl">
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-3xl font-bold text-white mb-4">
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
||||||
You have been logged out
|
{preserveSso ? 'Application Sign Out' : 'Complete Sign Out'}
|
||||||
</h2>
|
</h1>
|
||||||
|
|
||||||
{sessionStatus === 'checking' && (
|
{sessionStatus === 'checking' && (
|
||||||
<p className="text-white/80 mb-4">
|
<p className="text-gray-600">Cleaning up your session...</p>
|
||||||
Verifying all sessions are terminated...
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionStatus === 'cleared' && (
|
{sessionStatus === 'cleared' && (
|
||||||
<p className="text-white/80 mb-4">
|
<>
|
||||||
Your session has been completely terminated and all authentication data has been cleared.
|
<p className="text-gray-600 mb-4">{message}</p>
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionStatus === 'error' && (
|
{preserveSso ? (
|
||||||
<p className="text-white/80 mb-4">
|
<>
|
||||||
Your session has been terminated, but there might be some residual session data.
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
For complete security, please close your browser.
|
You can continue using other applications without signing in again.
|
||||||
</p>
|
</p>
|
||||||
)}
|
<div className="flex flex-col space-y-4">
|
||||||
|
|
||||||
<div className="mt-6">
|
|
||||||
<Link
|
<Link
|
||||||
href="/signin?fresh=true"
|
href="/"
|
||||||
className="inline-block px-8 py-3 bg-white text-gray-800 rounded hover:bg-gray-100 transition-colors mb-4"
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Return to Home Page
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.href = '/loggedout'}
|
||||||
|
className="inline-block px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
Sign Out Completely
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
You will need to sign in again to access any protected services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{keycloakLogoutUrl && (
|
||||||
|
<iframe
|
||||||
|
ref={iframeRef}
|
||||||
|
src={keycloakLogoutUrl}
|
||||||
|
style={{ width: '1px', height: '1px', position: 'absolute', top: '-100px', left: '-100px' }}
|
||||||
|
title="Keycloak Logout"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/api/auth/signin?fresh=true"
|
||||||
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
>
|
>
|
||||||
Sign In Again
|
Sign In Again
|
||||||
</Link>
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<p className="text-white/60 text-sm mt-4">
|
{sessionStatus === 'error' && (
|
||||||
Note: You'll need to enter your credentials when signing in again.
|
<>
|
||||||
</p>
|
<p className="text-red-600 mb-4">{message}</p>
|
||||||
</div>
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
Return to Home Page
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,51 +1,56 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
export default function SilentRefresh() {
|
export default function SilentRefresh() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const [message, setMessage] = useState('Checking authentication...');
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Notify parent window of authentication state
|
// Notify parent window of authentication status
|
||||||
const notifyParent = () => {
|
const notifyParent = (statusType: string) => {
|
||||||
try {
|
try {
|
||||||
|
// Post message to parent window
|
||||||
if (window.parent && window.parent !== window) {
|
if (window.parent && window.parent !== window) {
|
||||||
if (status === 'authenticated' && session) {
|
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: 'SILENT_AUTH_SUCCESS',
|
type: 'AUTH_STATUS',
|
||||||
session: {
|
status: statusType,
|
||||||
authenticated: true,
|
timestamp: Date.now()
|
||||||
userId: session.user.id,
|
|
||||||
username: session.user.username,
|
|
||||||
roles: session.user.role
|
|
||||||
}
|
|
||||||
}, '*');
|
}, '*');
|
||||||
setMessage('Authentication successful. You can close this window.');
|
|
||||||
} else if (status === 'unauthenticated') {
|
console.log(`Silent refresh: notified parent of ${statusType} status`);
|
||||||
window.parent.postMessage({
|
|
||||||
type: 'SILENT_AUTH_FAILURE',
|
|
||||||
error: 'Not authenticated'
|
|
||||||
}, '*');
|
|
||||||
setMessage('Not authenticated. You may need to log in again.');
|
|
||||||
}
|
}
|
||||||
}
|
} catch (error) {
|
||||||
} catch (e) {
|
console.error('Error notifying parent window:', error);
|
||||||
console.error('Error notifying parent window:', e);
|
|
||||||
setMessage('Error communicating with parent window.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status !== 'loading') {
|
// When session status changes, notify parent
|
||||||
notifyParent();
|
if (status === 'authenticated' && session) {
|
||||||
|
// User is authenticated
|
||||||
|
notifyParent('AUTHENTICATED');
|
||||||
|
} else if (status === 'unauthenticated') {
|
||||||
|
// User is not authenticated
|
||||||
|
notifyParent('UNAUTHENTICATED');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up automatic cleanup
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
// Notify parent we're cleaning up (in case we're in loading state forever)
|
||||||
|
notifyParent('CLEANUP');
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
}, [session, status]);
|
}, [session, status]);
|
||||||
|
|
||||||
// This page is meant to be loaded in an iframe, so keep it minimal
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px', fontFamily: 'sans-serif', color: '#666' }}>
|
<div className="p-4 text-center">
|
||||||
{message}
|
<h1 className="text-lg font-medium">Silent Authentication Check</h1>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
{status === 'loading' && 'Checking authentication status...'}
|
||||||
|
{status === 'authenticated' && 'You are authenticated.'}
|
||||||
|
{status === 'unauthenticated' && 'You are not authenticated.'}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -12,18 +12,21 @@ export function SignOutHandler() {
|
|||||||
try {
|
try {
|
||||||
// Store the user ID before signout clears the session
|
// Store the user ID before signout clears the session
|
||||||
const userId = session?.user?.id;
|
const userId = session?.user?.id;
|
||||||
console.log('Starting comprehensive logout process');
|
console.log('Starting optimized logout process (preserving SSO)');
|
||||||
|
|
||||||
// First trigger server-side session cleanup
|
// First trigger server-side Redis cleanup only
|
||||||
if (userId) {
|
if (userId) {
|
||||||
console.log('Triggering server-side session cleanup');
|
console.log('Triggering server-side Redis cleanup');
|
||||||
try {
|
try {
|
||||||
const cleanupResponse = await fetch('/api/auth/session-cleanup', {
|
const cleanupResponse = await fetch('/api/auth/session-cleanup', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ userId }),
|
body: JSON.stringify({
|
||||||
|
userId,
|
||||||
|
preserveSso: true
|
||||||
|
}),
|
||||||
credentials: 'include'
|
credentials: 'include'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,13 +34,32 @@ export function SignOutHandler() {
|
|||||||
console.log('Server cleanup result:', cleanupResult);
|
console.log('Server cleanup result:', cleanupResult);
|
||||||
} catch (cleanupError) {
|
} catch (cleanupError) {
|
||||||
console.error('Error during server-side cleanup:', cleanupError);
|
console.error('Error during server-side cleanup:', cleanupError);
|
||||||
// Continue with logout even if cleanup fails
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, attempt to sign out from NextAuth explicitly with force option
|
// Use NextAuth signOut but ONLY for our application cookies
|
||||||
|
// This preserves the Keycloak SSO session for other services
|
||||||
await signOut({ redirect: false });
|
await signOut({ redirect: false });
|
||||||
|
|
||||||
|
// Clear ONLY application-specific cookies (not Keycloak SSO cookies)
|
||||||
|
const appCookies = [
|
||||||
|
'next-auth.session-token',
|
||||||
|
'next-auth.csrf-token',
|
||||||
|
'next-auth.callback-url',
|
||||||
|
'__Secure-next-auth.session-token',
|
||||||
|
'__Host-next-auth.csrf-token',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Only clear chunked cookies for our app
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
const chunkedCookies = cookies
|
||||||
|
.map(cookie => cookie.split('=')[0].trim())
|
||||||
|
.filter(name => /next-auth.*\.\d+$/.test(name));
|
||||||
|
|
||||||
|
[...appCookies, ...chunkedCookies].forEach(cookieName => {
|
||||||
|
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${window.location.hostname}; SameSite=None; Secure`;
|
||||||
|
});
|
||||||
|
|
||||||
// Clear Rocket Chat authentication tokens
|
// Clear Rocket Chat authentication tokens
|
||||||
try {
|
try {
|
||||||
console.log('Clearing Rocket Chat tokens');
|
console.log('Clearing Rocket Chat tokens');
|
||||||
@ -48,139 +70,16 @@ export function SignOutHandler() {
|
|||||||
// Remove localStorage items
|
// Remove localStorage items
|
||||||
localStorage.removeItem('Meteor.loginToken');
|
localStorage.removeItem('Meteor.loginToken');
|
||||||
localStorage.removeItem('Meteor.userId');
|
localStorage.removeItem('Meteor.userId');
|
||||||
|
|
||||||
// Try to send logout to Rocket Chat server
|
|
||||||
const rocketChatBaseUrl = process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0];
|
|
||||||
if (rocketChatBaseUrl) {
|
|
||||||
// This is a best-effort logout - we don't wait for it to complete
|
|
||||||
fetch(`${rocketChatBaseUrl}/api/v1/logout`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
}).catch(e => console.error('Failed to notify Rocket Chat server about logout:', e));
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Error clearing Rocket Chat tokens:', e);
|
console.error('Error clearing Rocket Chat tokens:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then clear all auth-related cookies to ensure we break any local sessions
|
// Redirect to loggedout page
|
||||||
clearAuthCookies();
|
window.location.href = '/loggedout?preserveSso=true';
|
||||||
|
|
||||||
// Get Keycloak logout URL with additional parameters to force session expiration
|
|
||||||
if (process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER) {
|
|
||||||
console.log('Preparing complete Keycloak 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 than GET)
|
|
||||||
const form = document.createElement('form');
|
|
||||||
form.method = 'POST';
|
|
||||||
form.action = logoutEndpoint;
|
|
||||||
|
|
||||||
// Add id_token_hint if available
|
|
||||||
if (session?.accessToken) {
|
|
||||||
const tokenInput = document.createElement('input');
|
|
||||||
tokenInput.type = 'hidden';
|
|
||||||
tokenInput.name = 'id_token_hint';
|
|
||||||
tokenInput.value = session.accessToken;
|
|
||||||
form.appendChild(tokenInput);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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?forceLogout=true`;
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Add state parameter with random value to prevent CSRF
|
|
||||||
const stateInput = document.createElement('input');
|
|
||||||
stateInput.type = 'hidden';
|
|
||||||
stateInput.name = 'state';
|
|
||||||
stateInput.value = Math.random().toString(36).substring(2);
|
|
||||||
form.appendChild(stateInput);
|
|
||||||
|
|
||||||
// Set initiate_login_uri parameter to force login screen on next login
|
|
||||||
const initiateLoginInput = document.createElement('input');
|
|
||||||
initiateLoginInput.type = 'hidden';
|
|
||||||
initiateLoginInput.name = 'initiate_login_uri';
|
|
||||||
initiateLoginInput.value = `${window.location.origin}/signin?fresh=true`;
|
|
||||||
form.appendChild(initiateLoginInput);
|
|
||||||
|
|
||||||
// Add UI locales parameter
|
|
||||||
const uiLocalesInput = document.createElement('input');
|
|
||||||
uiLocalesInput.type = 'hidden';
|
|
||||||
uiLocalesInput.name = 'ui_locales';
|
|
||||||
uiLocalesInput.value = 'fr';
|
|
||||||
form.appendChild(uiLocalesInput);
|
|
||||||
|
|
||||||
// Notify iframe parents before logging out
|
|
||||||
try {
|
|
||||||
// Attempt to notify any iframes that might be using this authentication
|
|
||||||
if (window.parent && window.parent !== window) {
|
|
||||||
window.parent.postMessage({ type: 'LOGOUT_EVENT' }, '*');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error notifying parent of logout:', e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Completely remove all Keycloak session cookies before logout
|
|
||||||
// This helps prevent automatic re-login
|
|
||||||
const keycloakCookies = [
|
|
||||||
'KEYCLOAK_SESSION',
|
|
||||||
'KEYCLOAK_IDENTITY',
|
|
||||||
'KEYCLOAK_REMEMBER_ME',
|
|
||||||
'KC_RESTART',
|
|
||||||
'KEYCLOAK_SESSION_LEGACY',
|
|
||||||
'KEYCLOAK_IDENTITY_LEGACY',
|
|
||||||
'AUTH_SESSION_ID',
|
|
||||||
'AUTH_SESSION_ID_LEGACY',
|
|
||||||
'JSESSIONID'
|
|
||||||
];
|
|
||||||
|
|
||||||
const domains = [
|
|
||||||
window.location.hostname,
|
|
||||||
`.${window.location.hostname}`,
|
|
||||||
window.location.hostname.split('.').slice(-2).join('.'),
|
|
||||||
`.${window.location.hostname.split('.').slice(-2).join('.')}`
|
|
||||||
];
|
|
||||||
|
|
||||||
keycloakCookies.forEach(cookieName => {
|
|
||||||
domains.forEach(domain => {
|
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; domain=${domain}; SameSite=None; Secure`;
|
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/realms/; domain=${domain}; SameSite=None; Secure`;
|
|
||||||
document.cookie = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/auth/; domain=${domain}; SameSite=None; Secure`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
window.location.href = '/loggedout?forceLogout=true';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during logout:', error);
|
console.error('Error during logout:', error);
|
||||||
window.location.href = '/loggedout?forceLogout=true';
|
window.location.href = '/loggedout?preserveSso=true';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -196,7 +95,7 @@ export function SignOutHandler() {
|
|||||||
<div className="w-full h-full flex items-center justify-center">
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-2xl font-bold">Logging out...</h2>
|
<h2 className="text-2xl font-bold">Logging out...</h2>
|
||||||
<p className="text-gray-500 mt-2">Please wait while we sign you out completely.</p>
|
<p className="text-gray-500 mt-2">Please wait while we sign you out of this application.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
148
middleware.ts
148
middleware.ts
@ -1,80 +1,104 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import type { NextRequest } from 'next/server';
|
|
||||||
|
|
||||||
// Maximum cookie size in bytes (even more conservative to prevent chunking issues)
|
const config = {
|
||||||
const MAX_COOKIE_SIZE = 3500;
|
matcher: [
|
||||||
|
/*
|
||||||
|
* Match all paths except for:
|
||||||
|
* 1. /api routes
|
||||||
|
* 2. /_next (Next.js internals)
|
||||||
|
* 3. /_static (inside /public)
|
||||||
|
* 4. all root files inside /public (e.g. /favicon.ico)
|
||||||
|
*/
|
||||||
|
'/((?!api/|_next/|_static/|_vercel|[\\w-]+\\.\\w+).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
// This middleware runs before any request
|
export default async function middleware(req: NextRequest) {
|
||||||
export function middleware(request: NextRequest) {
|
const url = req.nextUrl;
|
||||||
// Force NextAuth environment variables at runtime to prevent cookie chunking
|
|
||||||
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = MAX_COOKIE_SIZE.toString();
|
|
||||||
|
|
||||||
// Disable cookie callbacks which can increase cookie size
|
|
||||||
process.env.NEXTAUTH_CALLBACK = 'false';
|
|
||||||
process.env.NEXTAUTH_SESSION_STORE_SESSION_TOKEN = 'false';
|
|
||||||
process.env.NEXTAUTH_JWT_STORE_RAW_TOKEN = 'false';
|
|
||||||
|
|
||||||
// Check if this is a logout-related request
|
|
||||||
const url = request.nextUrl.pathname;
|
|
||||||
const isLogoutPath = url.includes('/signout') || url.includes('/loggedout');
|
|
||||||
const isSigninPath = url.includes('/signin');
|
|
||||||
const hasForceLogoutParam = request.nextUrl.searchParams.has('forceLogout');
|
|
||||||
|
|
||||||
if (isLogoutPath || hasForceLogoutParam) {
|
|
||||||
// On logout pages, we want to ensure cookies are cleaned up
|
|
||||||
const response = NextResponse.next();
|
const response = NextResponse.next();
|
||||||
|
|
||||||
// List of authentication-related cookies to clear on logout
|
// Maximum size to prevent cookie chunking
|
||||||
const authCookies = [
|
const MAX_COOKIE_SIZE = 3500; // conservative limit in bytes
|
||||||
|
|
||||||
|
// Function to set all required nextAuth environment variables
|
||||||
|
const setNextAuthEnvVars = () => {
|
||||||
|
// Disable callbacks that could increase cookie size
|
||||||
|
process.env.NEXTAUTH_DISABLE_CALLBACK = 'true';
|
||||||
|
process.env.NEXTAUTH_DISABLE_JWT_CALLBACK = 'true';
|
||||||
|
process.env.NEXTAUTH_COOKIE_SIZE_LIMIT = String(MAX_COOKIE_SIZE);
|
||||||
|
process.env.NEXTAUTH_COOKIES_CHUNKING = 'true';
|
||||||
|
process.env.NEXTAUTH_COOKIES_CHUNKING_SIZE = String(MAX_COOKIE_SIZE);
|
||||||
|
process.env.NEXTAUTH_COOKIES_SECURE = 'true';
|
||||||
|
process.env.NEXTAUTH_COOKIES_SAMESITE = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set environment variables for all routes
|
||||||
|
setNextAuthEnvVars();
|
||||||
|
|
||||||
|
// Special handling for loggedout page to clean up cookies
|
||||||
|
if (url.pathname === '/loggedout') {
|
||||||
|
// Check if we're preserving SSO or doing a full logout
|
||||||
|
const preserveSso = url.searchParams.get('preserveSso') === 'true';
|
||||||
|
|
||||||
|
console.log(`Middleware detected logout (preserveSso: ${preserveSso})`);
|
||||||
|
|
||||||
|
if (preserveSso) {
|
||||||
|
// Only clean up NextAuth cookies but preserve Keycloak SSO cookies
|
||||||
|
const nextAuthCookies = [
|
||||||
'next-auth.session-token',
|
'next-auth.session-token',
|
||||||
'next-auth.callback-url',
|
|
||||||
'next-auth.csrf-token',
|
'next-auth.csrf-token',
|
||||||
'next-auth.pkce.code-verifier',
|
'next-auth.callback-url',
|
||||||
'__Secure-next-auth.session-token',
|
'__Secure-next-auth.session-token',
|
||||||
'__Host-next-auth.csrf-token'
|
'__Host-next-auth.csrf-token'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Clear each cookie with appropriate settings
|
nextAuthCookies.forEach(name => {
|
||||||
authCookies.forEach(cookieName => {
|
response.cookies.delete(name);
|
||||||
// Try to detect and clear chunked cookies too
|
});
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
const chunkName = i > 0 ? `${cookieName}.${i}` : cookieName;
|
|
||||||
|
|
||||||
// Clear with domain
|
// Also delete any chunked cookies
|
||||||
if (process.env.NEXTAUTH_COOKIE_DOMAIN) {
|
const cookieNames = Object.keys(req.cookies.getAll());
|
||||||
response.cookies.delete({
|
const chunkedCookies = cookieNames.filter(name => /next-auth.*\.\d+$/.test(name));
|
||||||
name: chunkName,
|
chunkedCookies.forEach(name => {
|
||||||
domain: process.env.NEXTAUTH_COOKIE_DOMAIN,
|
response.cookies.delete(name);
|
||||||
path: '/'
|
});
|
||||||
|
} else {
|
||||||
|
// Full logout - clear all auth-related cookies
|
||||||
|
const authCookies = [
|
||||||
|
'next-auth.session-token',
|
||||||
|
'next-auth.csrf-token',
|
||||||
|
'next-auth.callback-url',
|
||||||
|
'__Secure-next-auth.session-token',
|
||||||
|
'__Host-next-auth.csrf-token',
|
||||||
|
'KEYCLOAK_SESSION',
|
||||||
|
'KEYCLOAK_IDENTITY',
|
||||||
|
'KC_RESTART',
|
||||||
|
'JSESSIONID'
|
||||||
|
];
|
||||||
|
|
||||||
|
authCookies.forEach(name => {
|
||||||
|
response.cookies.delete(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also delete any chunked cookies
|
||||||
|
const cookieNames = Object.keys(req.cookies.getAll());
|
||||||
|
const chunkedCookies = cookieNames.filter(name =>
|
||||||
|
/next-auth.*\.\d+$/.test(name) ||
|
||||||
|
/KEYCLOAK.*/.test(name) ||
|
||||||
|
/KC_.*/.test(name)
|
||||||
|
);
|
||||||
|
chunkedCookies.forEach(name => {
|
||||||
|
response.cookies.delete(name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also clear without domain
|
|
||||||
response.cookies.delete({
|
|
||||||
name: chunkName,
|
|
||||||
path: '/'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// For sign-in page, add header if fresh login is requested
|
||||||
|
if (url.pathname === '/api/auth/signin' && url.searchParams.get('fresh') === 'true') {
|
||||||
|
response.headers.set('X-Auth-Fresh-Login', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
|
||||||
|
|
||||||
// For the signin page with fresh=true param, pass a header indicating fresh login
|
|
||||||
if (isSigninPath && request.nextUrl.searchParams.get('fresh') === 'true') {
|
|
||||||
const response = NextResponse.next();
|
|
||||||
response.headers.set('X-Force-Fresh-Login', 'true');
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue with the request for all other paths
|
|
||||||
return NextResponse.next();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the middleware to run on specific paths
|
export { config };
|
||||||
export const config = {
|
|
||||||
matcher: [
|
|
||||||
// Apply to all routes except static files
|
|
||||||
'/((?!_next/static|_next/image|favicon.ico|public).*)',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue
Block a user