From 1b737cbce5299b94689cc9ade60bf0f1d64a894c Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Fri, 19 Dec 2025 13:21:42 +0000 Subject: [PATCH] Fixing SAML logout --- .../common/model/ApplicationProperties.java | 1 + .../software/common/util/RequestUriUtils.java | 2 +- .../src/main/resources/settings.yml.template | 9 +- .../security/CustomLogoutSuccessHandler.java | 150 +++++------------- .../configuration/SecurityConfiguration.java | 42 ++++- .../controller/api/AuthController.java | 23 --- .../security/service/JwtService.java | 18 +++ .../public/Login/Microsoft-intune-logo.svg | 59 +++++++ .../src/proprietary/auth/springAuthClient.ts | 31 ++-- .../shared/config/OverviewHeader.tsx | 12 +- .../config/configSections/AccountSection.tsx | 9 +- frontend/vite.config.ts | 12 ++ 12 files changed, 210 insertions(+), 158 deletions(-) create mode 100644 frontend/public/Login/Microsoft-intune-logo.svg diff --git a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java index 68d4fcbcff..31e34f2468 100644 --- a/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java +++ b/app/common/src/main/java/stirling/software/common/model/ApplicationProperties.java @@ -227,6 +227,7 @@ public class ApplicationProperties { private String idpSingleLogoutUrl; private String idpSingleLoginUrl; private String idpIssuer; + private Boolean enableSingleLogout = false; @JsonProperty("idpCert") private String idpCert; diff --git a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java index d00e88eac2..e26df0c94f 100644 --- a/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java +++ b/app/common/src/main/java/stirling/software/common/util/RequestUriUtils.java @@ -165,7 +165,7 @@ public class RequestUriUtils { || trimmedUri.contains("/oauth2/authorization/") // OAuth2 authorization endpoint || trimmedUri.startsWith("/api/v1/auth/login") || trimmedUri.startsWith("/api/v1/auth/refresh") - || trimmedUri.startsWith("/api/v1/auth/logout") + || trimmedUri.startsWith("/logout") || trimmedUri.startsWith( "/api/v1/proprietary/ui-data/login") // Login page config (SSO providers + // enableLogin) diff --git a/app/core/src/main/resources/settings.yml.template b/app/core/src/main/resources/settings.yml.template index c0664a3951..fcdb833097 100644 --- a/app/core/src/main/resources/settings.yml.template +++ b/app/core/src/main/resources/settings.yml.template @@ -52,10 +52,11 @@ security: blockRegistration: false # set to 'true' to deny login with SSO without prior registration by an admin registrationId: stirling # The name of your Service Provider (SP) app name. Should match the name in the path for your SSO & SLO URLs idpMetadataUri: https://dev-XXXXXXXX.okta.com/app/externalKey/sso/saml/metadata # The uri for your Provider's metadata - idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your Provider - idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your Provider - idpIssuer: "" # The ID of your Provider - idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your Provider + idpSingleLoginUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/sso/saml # The URL for initiating SSO. Provided by your IdP + enableSingleLogout: true # set to 'true' to enable Single Logout (SP-initiated SLO). Logs the user out from the IdP + idpSingleLogoutUrl: https://dev-XXXXXXXX.okta.com/app/dev-XXXXXXXX_stirlingpdf_1/externalKey/slo/saml # The URL for initiating SLO. Provided by your IdP + idpIssuer: "" # The ID of your IdP + idpCert: classpath:okta.cert # The certificate your Provider will use to authenticate your app's SAML authentication requests. Provided by your IdP privateKey: classpath:saml-private-key.key # Your private key. Generated from your keypair spCert: classpath:saml-public-cert.crt # Your signing certificate. Generated from your keypair # IMPORTANT: For SAML setup, download your SP metadata from the BACKEND URL: http://localhost:8080/saml2/service-provider-metadata/{registrationId} 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 9f9247ff9b..0dabf4e760 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 @@ -9,47 +9,57 @@ import java.util.Locale; import java.util.Map; import java.util.Optional; -import org.springframework.core.io.Resource; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; -import com.coveo.saml.SamlClient; -import com.coveo.saml.SamlException; - import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.configuration.AppConfig; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.model.ApplicationProperties.Security.OAUTH2; -import stirling.software.common.model.ApplicationProperties.Security.SAML2; import stirling.software.common.model.oauth2.KeycloakProvider; import stirling.software.common.util.RegexPatternUtils; import stirling.software.common.util.UrlUtils; import stirling.software.proprietary.audit.AuditEventType; import stirling.software.proprietary.audit.AuditLevel; import stirling.software.proprietary.audit.Audited; -import stirling.software.proprietary.security.saml2.CertificateUtils; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; import stirling.software.proprietary.security.service.JwtServiceInterface; @Slf4j -@RequiredArgsConstructor public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public static final String LOGOUT_PATH = "/login?logout=true"; private final ApplicationProperties.Security securityProperties; - private final AppConfig appConfig; - private final JwtServiceInterface jwtService; + private final LogoutSuccessHandler samlLogoutHandler; + + public CustomLogoutSuccessHandler( + ApplicationProperties.Security securityProperties, + AppConfig appConfig, + JwtServiceInterface jwtService) { + this(securityProperties, appConfig, jwtService, null); + } + + public CustomLogoutSuccessHandler( + ApplicationProperties.Security securityProperties, + AppConfig appConfig, + JwtServiceInterface jwtService, + LogoutSuccessHandler samlLogoutHandler) { + this.securityProperties = securityProperties; + this.appConfig = appConfig; + this.jwtService = jwtService; + this.samlLogoutHandler = samlLogoutHandler; + } @Override @Audited(type = AuditEventType.USER_LOGOUT, level = AuditLevel.BASIC) @@ -58,16 +68,29 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { throws IOException { if (!response.isCommitted()) { + // Handle SAML2 SLO Optional samlNameId = resolveSamlNameId(authentication, request); if (samlNameId.isPresent()) { - getRedirect_saml2(request, response, samlNameId.get()); + if (samlLogoutHandler != null) { + log.info("SAML user {} logging out via IdP SLO", samlNameId.get()); + try { + samlLogoutHandler.onLogoutSuccess(request, response, authentication); + } catch (Exception e) { + log.error("SAML SLO failed, falling back to local logout", e); + getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); + } + } else { + // SAML Single Logout disabled - just do local logout + log.info("SAML user {} logging out locally (SLO disabled)", samlNameId.get()); + getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); + } return; } if (authentication != null) { if (authentication instanceof OAuth2AuthenticationToken oAuthToken) { // Handle OAuth2 logout redirection - getRedirect_oauth2(request, response, oAuthToken); + getRedirectOauth2(request, response, oAuthToken); } else if (authentication instanceof UsernamePasswordAuthenticationToken) { // Handle Username/Password logout getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); @@ -79,82 +102,20 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); } } else { - if (jwtService != null) { - String token = jwtService.extractToken(request); - if (token != null && !token.isBlank()) { - getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); - return; - } - } - // Redirect to login page after logout - String path = checkForErrors(request); - getRedirectStrategy().sendRedirect(request, response, path); + // Redirect to login page after logout (handles error parameters if present) + String queryParams = checkForErrors(request); + getRedirectStrategy().sendRedirect(request, response, "/login?" + queryParams); } } } - // Redirect for SAML2 authentication logout - private void getRedirect_saml2( - HttpServletRequest request, HttpServletResponse response, Saml2Authentication samlAuth) - throws IOException { - CustomSaml2AuthenticatedPrincipal principal = - (CustomSaml2AuthenticatedPrincipal) samlAuth.getPrincipal(); - getRedirect_saml2(request, response, principal.nameId()); - } - - private void getRedirect_saml2( - HttpServletRequest request, HttpServletResponse response, String nameIdValue) - throws IOException { - - if (nameIdValue == null || nameIdValue.isBlank()) { - log.warn("Unable to perform SAML logout due to missing NameID"); - getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); - return; - } - - SAML2 samlConf = securityProperties.getSaml2(); - String registrationId = samlConf.getRegistrationId(); - - try { - // Read certificate from the resource - Resource certificateResource = samlConf.getSpCert(); - X509Certificate certificate = CertificateUtils.readCertificate(certificateResource); - - List certificates = new ArrayList<>(); - certificates.add(certificate); - - // Construct URLs required for SAML configuration - SamlClient samlClient = getSamlClient(registrationId, samlConf, certificates); - - // Read private key for service provider - Resource privateKeyResource = samlConf.getPrivateKey(); - RSAPrivateKey privateKey = CertificateUtils.readPrivateKey(privateKeyResource); - - // Set service provider keys for the SamlClient - samlClient.setSPKeys(certificate, privateKey); - - // Build relay state to return user to login page after IdP logout - String relayState = - UrlUtils.getOrigin(request) + request.getContextPath() + LOGOUT_PATH; - - // Redirect to identity provider for logout with relay state - samlClient.redirectToIdentityProvider(response, relayState, nameIdValue); - } catch (Exception e) { - log.error( - "Error retrieving logout URL from Provider {} for user {}", - samlConf.getIdpIssuer(), - nameIdValue, - e); - getRedirectStrategy().sendRedirect(request, response, LOGOUT_PATH); - } - } - private Optional resolveSamlNameId( Authentication authentication, HttpServletRequest request) { if (authentication instanceof Saml2Authentication samlAuthentication) { CustomSaml2AuthenticatedPrincipal principal = (CustomSaml2AuthenticatedPrincipal) samlAuthentication.getPrincipal(); String nameId = principal.nameId(); + if (nameId != null && !nameId.isBlank()) { return Optional.of(nameId); } @@ -163,14 +124,15 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { if (jwtService != null) { try { String token = jwtService.extractToken(request); + if (token != null && !token.isBlank()) { Map claims = jwtService.extractClaims(token); Object authType = claims.get("authType"); + if (authType != null && "SAML2".equalsIgnoreCase(authType.toString())) { Object nameId = claims.get("samlNameId"); - if (nameId instanceof String && !((String) nameId).isBlank()) { - return Optional.of((String) nameId); - } + log.debug("Resolved SAML NameID from JWT: {}", nameId); + return Optional.of((String) nameId); } } } catch (Exception ex) { @@ -182,7 +144,7 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { } // Redirect for OAuth2 authentication logout - private void getRedirect_oauth2( + private void getRedirectOauth2( HttpServletRequest request, HttpServletResponse response, OAuth2AuthenticationToken oAuthToken) @@ -240,30 +202,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { } } - private SamlClient getSamlClient( - String registrationId, SAML2 samlConf, List certificates) - throws SamlException { - String serverUrl = appConfig.getBackendUrl() + ":" + appConfig.getServerPort(); - - String relyingPartyIdentifier = - serverUrl + "/saml2/service-provider-metadata/" + registrationId; - - String assertionConsumerServiceUrl = serverUrl + "/login/saml2/sso/" + registrationId; - - String idpSLOUrl = samlConf.getIdpSingleLogoutUrl(); - - String idpIssuer = samlConf.getIdpIssuer(); - - // Create SamlClient instance for SAML logout - return new SamlClient( - relyingPartyIdentifier, - assertionConsumerServiceUrl, - idpSLOUrl, - idpIssuer, - certificates, - SamlClient.SamlIdpBinding.POST); - } - /** * Handles different error scenarios during logout. Will return a String containing * the error request parameter. diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java index 924c7ca0c7..114a7c116e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/configuration/SecurityConfiguration.java @@ -20,11 +20,14 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; +import org.springframework.security.saml2.provider.service.authentication.logout.OpenSaml4LogoutRequestResolver; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.authentication.OpenSaml4AuthenticationRequestResolver; +import org.springframework.security.saml2.provider.service.web.authentication.logout.Saml2RelyingPartyInitiatedLogoutSuccessHandler; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.security.web.savedrequest.NullRequestCache; import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; @@ -227,6 +230,20 @@ public class SecurityConfiguration { return requestURI.startsWith(contextPath + "/api/"); })); + // Create SAML logout handler if SAML SLO is enabled + final LogoutSuccessHandler samlLogoutHandler; + if (securityProperties.isSaml2Active() + && Boolean.TRUE.equals(securityProperties.getSaml2().getEnableSingleLogout()) + && saml2RelyingPartyRegistrations != null) { + log.info("Creating SAML2 SLO handler for SP-initiated logout"); + OpenSaml4LogoutRequestResolver logoutRequestResolver = + new OpenSaml4LogoutRequestResolver(saml2RelyingPartyRegistrations); + samlLogoutHandler = + new Saml2RelyingPartyInitiatedLogoutSuccessHandler(logoutRequestResolver); + } else { + samlLogoutHandler = null; + } + http.logout( logout -> logout.logoutRequestMatcher( @@ -234,10 +251,17 @@ public class SecurityConfiguration { .matcher("/logout")) .logoutSuccessHandler( new CustomLogoutSuccessHandler( - securityProperties, appConfig, jwtService)) + securityProperties, + appConfig, + jwtService, + samlLogoutHandler)) .clearAuthentication(true) .invalidateHttpSession(true) - .deleteCookies("JSESSIONID", "remember-me", "stirling_jwt")); + .deleteCookies( + "JSESSIONID", + "remember-me", + "stirling_jwt", + "stirling_logout_token")); http.rememberMe( rememberMeConfigurer -> // Use the configurator directly rememberMeConfigurer @@ -361,6 +385,20 @@ public class SecurityConfiguration { } }) .saml2Metadata(metadata -> {}); + + // Configure SAML2 Single Logout if enabled + // This sets up endpoints for: + // - IdP-initiated logout: IdP sends LogoutRequest to /logout/saml2/slo + // - SP-initiated logout response: IdP sends LogoutResponse to /logout/saml2/slo + if (Boolean.TRUE.equals(securityProperties.getSaml2().getEnableSingleLogout())) { + log.info("SAML2 Single Logout (SLO) is enabled - configuring SLO endpoints"); + http.saml2Logout( + logout -> + logout.logoutUrl( + "/logout/saml2/slo") // URL that handles both request + // and response + ); + } } } else { log.debug("Login is not enabled."); diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java index 8c5e52dfe4..2986260d93 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/AuthController.java @@ -231,29 +231,6 @@ public class AuthController { } } - /** - * Logout endpoint - * - * @param response HTTP response - * @return Success message - */ - @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") - @PostMapping("/logout") - public ResponseEntity logout(HttpServletResponse response) { - try { - SecurityContextHolder.clearContext(); - - log.debug("User logged out successfully"); - - return ResponseEntity.ok(Map.of("message", "Logged out successfully")); - - } catch (Exception e) { - log.error("Logout error", e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(Map.of("error", "Internal server error")); - } - } - /** * Refresh token * diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java index 60472fef42..5605bc6745 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/service/JwtService.java @@ -258,6 +258,24 @@ public class JwtService implements JwtServiceInterface { return token; } + // Check for logout cookie (set by frontend before redirecting to /logout for SAML SLO) + if (request.getCookies() != null) { + for (jakarta.servlet.http.Cookie cookie : request.getCookies()) { + if ("stirling_logout_token".equals(cookie.getName())) { + String value = cookie.getValue(); + if (value != null && !value.isBlank()) { + try { + return java.net.URLDecoder.decode( + value, java.nio.charset.StandardCharsets.UTF_8); + } catch (Exception e) { + log.debug("Failed to decode logout token cookie", e); + return value; + } + } + } + } + } + return null; } diff --git a/frontend/public/Login/Microsoft-intune-logo.svg b/frontend/public/Login/Microsoft-intune-logo.svg new file mode 100644 index 0000000000..eb23892c39 --- /dev/null +++ b/frontend/public/Login/Microsoft-intune-logo.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/proprietary/auth/springAuthClient.ts b/frontend/src/proprietary/auth/springAuthClient.ts index 82deb34076..eede2a307c 100644 --- a/frontend/src/proprietary/auth/springAuthClient.ts +++ b/frontend/src/proprietary/auth/springAuthClient.ts @@ -309,18 +309,24 @@ class SpringAuthClient { /** * Sign out user (invalidate session) + * + * For SAML/OAuth users, this navigates to /logout which triggers + * Spring Security's logout handler and initiates SSO logout. + * The JWT is passed via a short-lived cookie so the backend can + * extract the SAML NameID for single logout (SLO). */ async signOut(): Promise<{ error: AuthError | null }> { try { - const response = await apiClient.post('/api/v1/auth/logout', null, { - headers: { - 'X-XSRF-TOKEN': this.getCsrfToken() || '', - }, - withCredentials: true, - }); + // Get the JWT before removing it - we need to pass it to the backend + // so it can extract the SAML NameID for single logout (SLO) + const token = localStorage.getItem('stirling_jwt'); - if (response.status === 200) { - // console.debug('[SpringAuth] signOut: Success'); + // Store JWT in a short-lived cookie for the logout request + // This avoids exposing the token in URL params while ensuring + // it's sent with the redirect request + if (token) { + // Cookie expires in 30 seconds - just long enough for the logout redirect + document.cookie = `stirling_logout_token=${encodeURIComponent(token)}; path=/; max-age=30; SameSite=Lax`; } // Clean up local storage @@ -355,13 +361,18 @@ class SpringAuthClient { console.warn('[SpringAuth] Failed to run platform auth cleanup', cleanupError); } - // Notify listeners + // Notify listeners before redirect this.notifyListeners('SIGNED_OUT', null); + // Navigate to /logout to trigger Spring Security's logout handler + console.log('[SpringAuth] Redirecting to /logout for session invalidation'); + window.location.href = `${BASE_PATH}/logout`; + + // This won't be reached if redirect happens, but return for type safety return { error: null }; } catch (error: unknown) { console.error('[SpringAuth] signOut error:', error); - // Still remove token even if backend call fails + // Still remove token even if the call fails localStorage.removeItem('stirling_jwt'); try { await clearPlatformAuthAfterSignOut(); diff --git a/frontend/src/proprietary/components/shared/config/OverviewHeader.tsx b/frontend/src/proprietary/components/shared/config/OverviewHeader.tsx index 2361fce970..4024ec0397 100644 --- a/frontend/src/proprietary/components/shared/config/OverviewHeader.tsx +++ b/frontend/src/proprietary/components/shared/config/OverviewHeader.tsx @@ -1,20 +1,16 @@ import { Text, Button } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import { useAuth } from '@app/auth/UseSession'; -import { useNavigate } from 'react-router-dom'; export function OverviewHeader() { const { t } = useTranslation(); const { signOut, user } = useAuth(); - const navigate = useNavigate(); const handleLogout = async () => { - try { - await signOut(); - navigate('/login'); - } catch (error) { - console.error('Logout error:', error); - } + console.log('[OverviewHeader] Logout button clicked, calling signOut()'); + // signOut() handles navigation to /logout for SAML/OAuth SLO + // Do NOT navigate after signOut - it will redirect the page + await signOut(); }; return ( diff --git a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx index a5fc29e1ac..8bffef50cf 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AccountSection.tsx @@ -53,11 +53,12 @@ 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 () => { + console.log('[AccountSection] Logout button clicked, calling signOut()'); + // signOut() handles navigation to /logout for SAML/OAuth SLO + // Do NOT navigate after signOut - it will redirect the page + await signOut(); + }, [signOut]); await accountLogout({ signOut, redirectToLogin }); }, [accountLogout, redirectToLogin, signOut]); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ad269a19d4..363079bb0b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -78,6 +78,18 @@ export default defineConfig(({ mode }) => { secure: false, xfwd: true, }, + '/logout': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + xfwd: true, + }, + '/logout/saml2': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + xfwd: true, + }, '/swagger-ui': { target: 'http://localhost:8080', changeOrigin: true,