From ec72cfd7e25dc17fc8ce933fa3f13d1291ac4da0 Mon Sep 17 00:00:00 2001 From: Dario Ghunney Ware Date: Thu, 11 Dec 2025 13:44:18 +0000 Subject: [PATCH] removing /logout from filters --- .../software/common/util/RequestUriUtils.java | 1 - .../security/CustomLogoutSuccessHandler.java | 68 +++++++++++++++++-- .../filter/JwtAuthenticationFilter.java | 36 +++++++--- frontend/vite.config.ts | 6 ++ 4 files changed, 97 insertions(+), 14 deletions(-) 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 f40a646cc7..3f25bba8f7 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 @@ -158,7 +158,6 @@ public class RequestUriUtils { // Public auth endpoints that don't require authentication return trimmedUri.startsWith("/login") - || trimmedUri.startsWith("/logout") || trimmedUri.startsWith("/auth/") || trimmedUri.startsWith("/oauth2") || trimmedUri.startsWith("/saml2") 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 b019652e5e..7c19200d1d 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 @@ -13,6 +13,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; 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; @@ -244,7 +245,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { // Redirect based on OAuth2 provider switch (registrationId.toLowerCase(Locale.ROOT)) { - case "keycloak" -> handleOidcLogout(request, response, oAuthToken, oauth, redirectUrl); case "github", "google" -> { // These providers don't support OIDC logout log.info( @@ -253,10 +253,69 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { redirectUrl); response.sendRedirect(redirectUrl); } - default -> { - // Try generic OIDC logout for any other provider - handleOidcLogout(request, response, oAuthToken, oauth, redirectUrl); + default -> handleOidcLogout(response, oAuthToken, oauth, redirectUrl); + } + } + + // Redirect for JWT-based OAuth2 authentication logout + private void handleJwtOAuth2Logout(HttpServletRequest request, HttpServletResponse response) + throws IOException { + OAUTH2 oauth = securityProperties.getOauth2(); + String path = checkForErrors(request); + String redirectUrl = UrlUtils.getOrigin(request) + "/login?" + path; + + // For JWT-based auth, we don't have OAuth2AuthenticationToken + // Attempt generic OIDC logout + String issuer = null; + String clientId = null; + + if (oauth.getClient() != null && oauth.getClient().getKeycloak() != null) { + KeycloakProvider keycloak = oauth.getClient().getKeycloak(); + if (keycloak.getIssuer() != null && !keycloak.getIssuer().isBlank()) { + issuer = keycloak.getIssuer(); + clientId = keycloak.getClientId(); + } else if (oauth.getIssuer() != null && !oauth.getIssuer().isBlank()) { + issuer = oauth.getIssuer(); + clientId = oauth.getClientId(); } + } else if (oauth.getIssuer() != null && !oauth.getIssuer().isBlank()) { + issuer = oauth.getIssuer(); + clientId = oauth.getClientId(); + } + + 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); + } + + // If we have an endpoint, construct the logout URL + if (endSessionEndpoint != null) { + StringBuilder logoutUrlBuilder = new StringBuilder(endSessionEndpoint); + logoutUrlBuilder.append(endSessionEndpoint.contains("?") ? "&" : "?"); + + // Without OAuth2AuthenticationToken, we don't have id_token_hint + // Just use client_id and post_logout_redirect_uri + if (clientId != null && !clientId.isBlank()) { + logoutUrlBuilder.append("client_id=").append(clientId); + logoutUrlBuilder.append("&"); + } + logoutUrlBuilder + .append("post_logout_redirect_uri=") + .append(response.encodeRedirectURL(redirectUrl)); + + String logoutUrl = logoutUrlBuilder.toString(); + log.info("JWT-based OAuth2 logout URL: {}", logoutUrl); + response.sendRedirect(logoutUrl); + } else { + // No OIDC logout endpoint available - fallback to local logout + log.info( + "No OIDC logout endpoint available for issuer: {}. Using local logout: {}", + issuer, + redirectUrl); + response.sendRedirect(redirectUrl); } } @@ -265,7 +324,6 @@ public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { * Discovered endpoint 3. Keycloak fallback (if isKeycloak=true) 4. Local logout */ private void handleOidcLogout( - HttpServletRequest request, HttpServletResponse response, OAuth2AuthenticationToken oAuthToken, OAUTH2 oauth, diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java index af37985679..8667421f66 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/security/filter/JwtAuthenticationFilter.java @@ -6,17 +6,20 @@ import static stirling.software.proprietary.security.model.AuthenticationType.OA import static stirling.software.proprietary.security.model.AuthenticationType.SAML2; import static stirling.software.proprietary.security.model.AuthenticationType.WEB; +import io.jsonwebtoken.Jwts; + import java.io.IOException; import java.sql.SQLException; import java.util.Map; import java.util.Optional; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.web.filter.OncePerRequestFilter; @@ -26,7 +29,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; @@ -40,7 +42,6 @@ import stirling.software.proprietary.security.service.JwtServiceInterface; import stirling.software.proprietary.security.service.UserService; @Slf4j -@RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtServiceInterface jwtService; @@ -49,6 +50,19 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final AuthenticationEntryPoint authenticationEntryPoint; private final ApplicationProperties.Security securityProperties; + public JwtAuthenticationFilter( + JwtServiceInterface jwtService, + UserService userService, + CustomUserDetailsService userDetailsService, + AuthenticationEntryPoint authenticationEntryPoint, + ApplicationProperties.Security securityProperties) { + this.jwtService = jwtService; + this.userService = userService; + this.userDetailsService = userDetailsService; + this.authenticationEntryPoint = authenticationEntryPoint; + this.securityProperties = securityProperties; + } + @Override protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) @@ -111,7 +125,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String tokenUsername = claims.get("sub").toString(); try { - authenticate(request, claims); + authenticate(request, jwtToken, claims); } catch (SQLException | UnsupportedProviderException e) { log.error("Error processing user authentication for user: {}", tokenUsername, e); handleAuthenticationFailure( @@ -165,7 +179,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { return true; } - private void authenticate(HttpServletRequest request, Map claims) + private void authenticate( + HttpServletRequest request, String jwtToken, Map claims) throws SQLException, UnsupportedProviderException { String username = claims.get("sub").toString(); @@ -174,9 +189,14 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (userDetails != null) { - UsernamePasswordAuthenticationToken authToken = - new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); + Jwt jwt = + Jwt.withTokenValue(jwtToken) + .headers(headers -> headers.put("alg", Jwts.SIG.RS256.getId())) + .claims(claimsMap -> claimsMap.putAll(claims)) + .build(); + + JwtAuthenticationToken authToken = + new JwtAuthenticationToken(jwt, userDetails.getAuthorities()); authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); log.debug("Setting authentication for user: {}", username); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 363079bb0b..2f34ab6253 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -102,6 +102,12 @@ export default defineConfig(({ mode }) => { secure: false, xfwd: true, }, + '/logout': { + target: 'http://localhost:8080', + changeOrigin: true, + secure: false, + xfwd: true, + }, }, }, base: process.env.RUN_SUBPATH ? `/${process.env.RUN_SUBPATH}` : './',