diff --git a/app/api/auth/end-sso-session/route.ts b/app/api/auth/end-sso-session/route.ts new file mode 100644 index 00000000..a05d4f10 --- /dev/null +++ b/app/api/auth/end-sso-session/route.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth/next'; +import { authOptions } from '../options'; +import { getKeycloakAdminClient } from '@/lib/keycloak'; +import { jwtDecode } from 'jwt-decode'; + +/** + * API endpoint to end the Keycloak SSO session for the current user + * This uses Keycloak Admin API to explicitly logout the user from all sessions, + * which clears the realm-wide SSO session, not just the client session. + * + * This ensures that when a user logs out from the dashboard, they are also + * logged out from all other applications that share the same Keycloak realm. + */ +export async function POST(request: NextRequest) { + try { + // Get the current session + const session = await getServerSession(authOptions); + + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Unauthorized', message: 'No active session' }, + { status: 401 } + ); + } + + // Get the ID token to extract user information + const idToken = session.idToken; + if (!idToken) { + return NextResponse.json( + { error: 'Missing ID token', message: 'Cannot end SSO session without ID token' }, + { status: 400 } + ); + } + + // Decode the ID token to get the user's Keycloak subject (user ID) + let userId: string; + try { + const decoded = jwtDecode<{ sub: string }>(idToken); + userId = decoded.sub; + } catch (error) { + console.error('Error decoding ID token:', error); + return NextResponse.json( + { error: 'Invalid ID token', message: 'Failed to decode ID token' }, + { status: 400 } + ); + } + + // Get Keycloak Admin Client + let adminClient; + try { + adminClient = await getKeycloakAdminClient(); + } catch (error) { + console.error('Error getting Keycloak admin client:', error); + return NextResponse.json( + { error: 'Keycloak admin error', message: 'Failed to connect to Keycloak admin API' }, + { status: 500 } + ); + } + + // Logout the user from all sessions using Admin API + // This will end the SSO session, not just the client session + try { + await adminClient.users.logout({ id: userId }); + console.log(`Successfully ended SSO session for user: ${userId}`); + + return NextResponse.json({ + success: true, + message: 'SSO session ended successfully', + userId: userId + }); + } catch (error: any) { + console.error('Error ending SSO session:', error); + + // If the error is that the user doesn't exist or session doesn't exist, + // that's okay - they're already logged out + if (error?.response?.status === 404 || error?.status === 404) { + return NextResponse.json({ + success: true, + message: 'User session not found (already logged out)', + userId: userId + }); + } + + return NextResponse.json( + { + error: 'Failed to end SSO session', + message: error?.message || 'Unknown error', + details: error?.response?.data || error + }, + { status: 500 } + ); + } + } catch (error) { + console.error('Unexpected error in end-sso-session endpoint:', error); + return NextResponse.json( + { error: 'Internal server error', message: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} + diff --git a/components/auth/signout-handler.tsx b/components/auth/signout-handler.tsx index 4f483468..8bcac8b0 100644 --- a/components/auth/signout-handler.tsx +++ b/components/auth/signout-handler.tsx @@ -24,6 +24,30 @@ export function SignOutHandler() { // Also attempt to clear Keycloak cookies clearKeycloakCookies(); + // End SSO session using Admin API before signing out + // This ensures the realm-wide SSO session is cleared, + // not just the client session + try { + const ssoLogoutResponse = await fetch('/api/auth/end-sso-session', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (ssoLogoutResponse.ok) { + console.log('SSO session ended successfully'); + } else { + const errorData = await ssoLogoutResponse.json().catch(() => ({})); + console.warn('Failed to end SSO session via Admin API, continuing with standard logout:', errorData); + // Continue with logout even if SSO session termination fails + } + } catch (error) { + console.error('Error ending SSO session:', error); + // Continue with logout even if SSO session termination fails + } + // Sign out from NextAuth (clears NextAuth session) await signOut({ callbackUrl: "/signin?logout=true", diff --git a/components/layout/layout-wrapper.tsx b/components/layout/layout-wrapper.tsx index b10098c6..23375f17 100644 --- a/components/layout/layout-wrapper.tsx +++ b/components/layout/layout-wrapper.tsx @@ -41,6 +41,30 @@ export function LayoutWrapper({ children, isSignInPage, isAuthenticated }: Layou clearAuthCookies(); clearKeycloakCookies(); + // End SSO session using Admin API before signing out + // This ensures the realm-wide SSO session is cleared, + // not just the client session + try { + const ssoLogoutResponse = await fetch('/api/auth/end-sso-session', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (ssoLogoutResponse.ok) { + console.log('SSO session ended successfully'); + } else { + const errorData = await ssoLogoutResponse.json().catch(() => ({})); + console.warn('Failed to end SSO session via Admin API, continuing with standard logout:', errorData); + // Continue with logout even if SSO session termination fails + } + } catch (error) { + console.error('Error ending SSO session:', error); + // Continue with logout even if SSO session termination fails + } + // Sign out from NextAuth await signOut({ callbackUrl: '/signin?logout=true', diff --git a/components/main-nav.tsx b/components/main-nav.tsx index 13945397..65c82b45 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -376,6 +376,30 @@ export function MainNav() { // Also attempt to clear Keycloak cookies clearKeycloakCookies(); + // End SSO session using Admin API before signing out + // This ensures the realm-wide SSO session is cleared, + // not just the client session + try { + const ssoLogoutResponse = await fetch('/api/auth/end-sso-session', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (ssoLogoutResponse.ok) { + console.log('SSO session ended successfully'); + } else { + const errorData = await ssoLogoutResponse.json().catch(() => ({})); + console.warn('Failed to end SSO session via Admin API, continuing with standard logout:', errorData); + // Continue with logout even if SSO session termination fails + } + } catch (error) { + console.error('Error ending SSO session:', error); + // Continue with logout even if SSO session termination fails + } + // Sign out from NextAuth (clears NextAuth session) await signOut({ callbackUrl: '/signin?logout=true',