keycloak improve with build 5

This commit is contained in:
alma 2026-01-02 16:23:30 +01:00
parent 43be7a99d2
commit f0c109ed8e
4 changed files with 174 additions and 0 deletions

View File

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

View File

@ -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",

View File

@ -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',

View File

@ -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',