From d4ac6148d8c545bc18f835eb37d88f0233b4d3ff Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Mon, 15 Dec 2025 17:59:38 +0000 Subject: [PATCH] moar cleanup --- .../security/CustomLogoutSuccessHandler.java | 28 ++----------------- .../CustomLogoutSuccessHandlerTest.java | 16 +++++------ .../src/proprietary/auth/springAuthClient.ts | 21 +++++++------- .../config/configSections/AccountSection.tsx | 12 ++------ 4 files changed, 23 insertions(+), 54 deletions(-) diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java index 5f6868537..283141663 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/CustomLogoutSuccessHandler.java @@ -65,15 +65,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { if (!response.isCommitted()) { if (authentication != null) { - // Check for JWT-based authentication and extract authType claim - String authType; - if (authentication instanceof JwtAuthenticationToken jwtAuth) { - authType = - (String) jwtAuth.getToken().getClaims().getOrDefault("authType", null); - - log.debug("JWT-based logout detected with authType: {}", authType); - } - if (authentication instanceof OAuth2AuthenticationToken oAuthToken) { log.info("OAuth2 logout via JWT - attempting OIDC logout"); getAuthRedirect(request, response, oAuthToken); @@ -81,7 +72,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { getAuthRedirect(request, response, samlAuthentication); } else if (authentication instanceof JwtAuthenticationToken jwtAuthenticationToken) { - getAuthRedirect(request, response, jwtAuthenticationToken); + getAuthRedirect(request, response); } else if (authentication instanceof UsernamePasswordAuthenticationToken) { // Handle Username/Password logout getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); @@ -186,10 +177,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { } // Redirect for JWT-based OAuth2 authentication logout - private void getAuthRedirect( - HttpServletRequest request, - HttpServletResponse response, - JwtAuthenticationToken jwtAuthenticationToken) + private void getAuthRedirect(HttpServletRequest request, HttpServletResponse response) throws IOException { OAUTH2 oauth = securityProperties.getOauth2(); String path = checkForErrors(request); @@ -213,18 +201,8 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { clientId = oauth.getClientId(); } - // Fallback: extract issuer from JWT token if not found in configuration - if (issuer == null) { - var jwtIssuer = jwtAuthenticationToken.getToken().getIssuer(); - if (jwtIssuer != null) { - issuer = jwtIssuer.toString(); - log.debug("Using issuer from JWT token: {}", issuer); - } - } - String endSessionEndpoint = getEndSessionEndpoint(oauth, issuer); - // If no endpoint found, try Keycloak fallback if (endSessionEndpoint == null && issuer != null) { endSessionEndpoint = issuer + "/protocol/openid-connect/logout"; log.debug("Using Keycloak fallback logout path: {}", endSessionEndpoint); @@ -242,7 +220,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { logoutUrlBuilder.append("post_logout_redirect_uri=").append(encodedRedirectUri); String logoutUrl = logoutUrlBuilder.toString(); - log.info("JWT-based OAuth2 logout URL: {}", logoutUrl); + log.debug("JWT-based OAuth2 logout URL: {}", logoutUrl); // Return JSON for API requests, redirect for browser requests if (isApi) { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java index f7d182d40..4ddd057a5 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/CustomLogoutSuccessHandlerTest.java @@ -1009,8 +1009,8 @@ class CustomLogoutSuccessHandlerTest { when(request.getHeader("X-Requested-With")).thenReturn(null); when(response.getWriter()).thenReturn(printWriter); - when(jwtAuth.getToken()).thenReturn(jwt); - when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); + lenient().when(jwtAuth.getToken()).thenReturn(jwt); + lenient().when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); when(securityProperties.getOauth2()).thenReturn(oauth); when(oauth.getClient()).thenReturn(client); @@ -1067,8 +1067,8 @@ class CustomLogoutSuccessHandlerTest { when(request.getHeader("X-Requested-With")).thenReturn("XMLHttpRequest"); // XHR request when(response.getWriter()).thenReturn(printWriter); - when(jwtAuth.getToken()).thenReturn(jwt); - when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); + lenient().when(jwtAuth.getToken()).thenReturn(jwt); + lenient().when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); when(securityProperties.getOauth2()).thenReturn(oauth); when(oauth.getClient()).thenReturn(client); @@ -1115,8 +1115,8 @@ class CustomLogoutSuccessHandlerTest { when(request.getHeader("Accept")).thenReturn("text/html"); // Browser request when(request.getHeader("X-Requested-With")).thenReturn(null); - when(jwtAuth.getToken()).thenReturn(jwt); - when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); + lenient().when(jwtAuth.getToken()).thenReturn(jwt); + lenient().when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); when(securityProperties.getOauth2()).thenReturn(oauth); when(oauth.getClient()).thenReturn(client); @@ -1164,8 +1164,8 @@ class CustomLogoutSuccessHandlerTest { when(request.getHeader("X-Requested-With")).thenReturn(null); when(response.getWriter()).thenReturn(printWriter); - when(jwtAuth.getToken()).thenReturn(jwt); - when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); + lenient().when(jwtAuth.getToken()).thenReturn(jwt); + lenient().when(jwt.getClaims()).thenReturn(Map.of("authType", "OAUTH2")); when(securityProperties.getOauth2()).thenReturn(oauth); when(oauth.getClient()).thenReturn(client); diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 4896ccecf..ecaa25dce 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -284,15 +284,9 @@ class SpringAuthClient { */ async signOut(): Promise<{ error: AuthError | null }> { try { - // Extract token before removing it (needed for OAuth/SAML logout) + // Extract token before making request (needed for OAuth/SAML logout) const token = localStorage.getItem('stirling_jwt'); - // Clean up local storage before logout - localStorage.removeItem('stirling_jwt'); - - // Call Spring Security's logout endpoint - // This will handle OAuth2/SAML logout and return JSON with logout URL for OIDC providers - // Include the token so Spring knows which authentication type to logout from const response = await apiClient.post('/logout', null, { headers: { ...(token ? { 'Authorization': `Bearer ${token}` } : {}), @@ -301,19 +295,24 @@ class SpringAuthClient { withCredentials: true, }); - // Notify listeners - this.notifyListeners('SIGNED_OUT', null); + // Clean up local storage after successful logout request + localStorage.removeItem('stirling_jwt'); - // If we got a logout URL (for OIDC/OAuth2 providers), navigate to it + // If we got a logout URL (for OIDC/OAuth2 providers), redirect immediately + // Don't notify listeners to avoid React re-render before redirect if (response.data?.logoutUrl) { window.location.href = response.data.logoutUrl; + return { error: null }; } + this.notifyListeners('SIGNED_OUT', null); + window.location.href = '/login?logout=true'; return { error: null }; } catch (error: unknown) { console.error('[SpringAuth] signOut error:', error); - // Still notify listeners even if backend call fails + localStorage.removeItem('stirling_jwt'); this.notifyListeners('SIGNED_OUT', null); + window.location.href = '/login?logout=true'; return { error: { message: getErrorMessage(error, 'Logout failed') }, }; diff --git a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx index 95a9ee57f..2316e0efb 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx @@ -37,17 +37,9 @@ const AccountSection: React.FC = () => { const userIdentifier = useMemo(() => user?.email || user?.username || '', [user?.email, user?.username]); - const redirectToLogin = useCallback(() => { - window.location.assign('/login'); - }, []); - const handleLogout = useCallback(async () => { - try { - await signOut(); - } finally { - redirectToLogin(); - } - }, [redirectToLogin, signOut]); + await signOut(); + }, [signOut]); const handlePasswordSubmit = async (event: React.FormEvent) => { event.preventDefault();