diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java index 263634aa..c131d84b 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/compat/WellKnownController.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.compat; +import com.iflytek.skillhub.auth.oauth.OAuthLoginRedirectSupport; import jakarta.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @@ -11,14 +12,8 @@ import java.util.Map; */ @RestController public class WellKnownController { - @GetMapping("/.well-known/clawhub.json") public Map clawhubConfig(HttpServletRequest request) { - // Honor the deployment sub-path so CLI clients discover //api/v1 instead of - // the domain-root /api/v1. With forward-headers-strategy=framework, an upstream - // X-Forwarded-Prefix is reflected into the request context path. - String contextPath = request.getContextPath(); - String prefix = (contextPath == null) ? "" : contextPath; - return Map.of("apiBase", prefix + "/api/v1"); + return Map.of("apiBase", OAuthLoginRedirectSupport.apiBase(request)); } } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java index 7f5154a4..354c1502 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/config/SecurityConfig.java @@ -4,6 +4,7 @@ import com.iflytek.skillhub.auth.oauth.CustomOAuth2UserService; import com.iflytek.skillhub.auth.oauth.CustomOidcUserService; import com.iflytek.skillhub.auth.oauth.OAuth2LoginFailureHandler; import com.iflytek.skillhub.auth.oauth.OAuth2LoginSuccessHandler; +import com.iflytek.skillhub.auth.oauth.OAuthLoginRedirectSupport; import com.iflytek.skillhub.auth.oauth.SkillHubOAuth2AuthorizationRequestResolver; import com.iflytek.skillhub.auth.mock.MockAuthFilter; import com.iflytek.skillhub.auth.policy.RouteSecurityPolicyRegistry; @@ -152,12 +153,8 @@ public class SecurityConfig { ) .logout(logout -> logout .logoutUrl("/api/v1/auth/logout") - // Redirect to the deployment root honoring any sub-path prefix (X-Forwarded-Prefix - // is reflected into the context path), so logout does not escape a sub-path deployment. - .logoutSuccessHandler((request, response, authentication) -> { - String contextPath = request.getContextPath(); - response.sendRedirect(((contextPath == null) ? "" : contextPath) + "/"); - }) + .logoutSuccessHandler((request, response, authentication) -> + response.sendRedirect(OAuthLoginRedirectSupport.webRoot(request))) .invalidateHttpSession(true) .deleteCookies("SESSION") ) diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java index feb60816..c8e369f5 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/oauth/OAuthLoginRedirectSupport.java @@ -1,7 +1,10 @@ package com.iflytek.skillhub.auth.oauth; +import jakarta.servlet.http.HttpServletRequest; + /** - * Utility methods and constants for safely handling post-login redirect targets in OAuth flows. + * Utility methods and constants for safely handling OAuth redirect targets and + * deployment-path aware browser-visible URLs. */ public final class OAuthLoginRedirectSupport { @@ -11,6 +14,30 @@ public final class OAuthLoginRedirectSupport { private OAuthLoginRedirectSupport() { } + public static String apiBase(HttpServletRequest request) { + return deploymentPrefix(request) + "/api/v1"; + } + + public static String webRoot(HttpServletRequest request) { + return deploymentPrefix(request) + "/"; + } + + static String deploymentPrefix(HttpServletRequest request) { + if (request == null) { + return ""; + } + String rawPrefix = request.getContextPath(); + if (rawPrefix == null || rawPrefix.isBlank() || "/".equals(rawPrefix)) { + return ""; + } + String prefix = rawPrefix.endsWith("/") ? rawPrefix.substring(0, rawPrefix.length() - 1) : rawPrefix; + if (!prefix.startsWith("/") || prefix.contains("//") || prefix.contains("\\") + || prefix.contains("?") || prefix.contains("#")) { + return ""; + } + return prefix; + } + public static String sanitizeReturnTo(String candidate) { if (candidate == null || candidate.isBlank()) { return null; diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginHandlersTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginHandlersTest.java index de3a8411..2e46908f 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginHandlersTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/oauth/OAuth2LoginHandlersTest.java @@ -108,6 +108,15 @@ class OAuth2LoginHandlersTest { assertThat(response.getRedirectedUrl()).isEqualTo("/skillhub/dashboard/publish"); } + @Test + void deploymentPathSupport_returnsPrefixedApiAndWebRoot() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setContextPath("/skillhub"); + + assertThat(OAuthLoginRedirectSupport.apiBase(request)).isEqualTo("/skillhub/api/v1"); + assertThat(OAuthLoginRedirectSupport.webRoot(request)).isEqualTo("/skillhub/"); + } + /** * Regression test: when an unauthenticated client hits a protected API endpoint, Spring Security * caches that request. With {@code SavedRequestAwareAuthenticationSuccessHandler} the post-login