From eef2fdc29f401db5b86d69ab5844498345d51044 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 9 Jun 2026 19:23:53 +0000 Subject: [PATCH 1/3] fix: repair ChatGPT MCP OAuth integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add path-aware /.well-known/oauth-authorization-server/** controller (RFC 8414 §3.1) so ChatGPT's discovery request for the /mcp suffix returns a valid JSON document instead of 404. - Include "none" in token_endpoint_auth_methods_supported via both the new AS metadata controller and the authorizationServerMetadataEndpoint customizer, so ChatGPT does not reject PKCE public-client flows. - Expose /.well-known/oauth-authorization-server/** as a public path in the main security chain. - Lock down /connect/register with denyAll() in both security chains to prevent unauthenticated dynamic client registration. https://claude.ai/code/session_01KUx3DVuxMyfGQaRCsfVtfx --- .../config/AuthorizationServerConfig.java | 7 +- ...AuthorizationServerMetadataController.java | 76 +++++++++++++++++++ .../com/jobtracker/config/SecurityConfig.java | 5 +- 3 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/jobtracker/config/OAuthAuthorizationServerMetadataController.java diff --git a/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java b/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java index 360804c..33bfb39 100644 --- a/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java +++ b/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java @@ -100,6 +100,7 @@ public SecurityFilterChain authorizationServerSecurityFilterChain( .securityMatcher(authServerMatcher) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/login", "/default-ui.css").permitAll() + .requestMatchers("/connect/register").denyAll() .anyRequest().authenticated()) .with(authorizationServerConfigurer, authorizationServer -> authorizationServer .oidc(oidc -> oidc @@ -114,7 +115,11 @@ public SecurityFilterChain authorizationServerSecurityFilterChain( metadata.tokenEndpointAuthenticationMethod("none"); })) .userInfoEndpoint(userInfo -> userInfo - .userInfoMapper(userInfoMapper)))) + .userInfoMapper(userInfoMapper))) + .authorizationServerMetadataEndpoint(metadata -> + metadata.authorizationServerMetadataCustomizer(builder -> + builder.tokenEndpointAuthenticationMethod( + ClientAuthenticationMethod.NONE.getValue())))) .exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML))) diff --git a/src/main/java/com/jobtracker/config/OAuthAuthorizationServerMetadataController.java b/src/main/java/com/jobtracker/config/OAuthAuthorizationServerMetadataController.java new file mode 100644 index 0000000..9e0cc59 --- /dev/null +++ b/src/main/java/com/jobtracker/config/OAuthAuthorizationServerMetadataController.java @@ -0,0 +1,76 @@ +package com.jobtracker.config; + +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * RFC 8414 path-aware Authorization Server Metadata. + * + * Spring Authorization Server only serves /.well-known/oauth-authorization-server at the + * root (no path suffix). ChatGPT follows the path-aware variant from RFC 8414 §3.1 and + * appends the protected-resource path suffix, so it requests: + * GET /.well-known/oauth-authorization-server/mcp + * which the built-in endpoint never matches. This controller fills that gap and also + * ensures "none" appears in token_endpoint_auth_methods_supported so public PKCE clients + * are not rejected before the flow starts. + */ +@RestController +public class OAuthAuthorizationServerMetadataController { + + private static final String WELL_KNOWN_PREFIX = "/.well-known/oauth-authorization-server"; + + private final AuthorizationServerSettings settings; + private final McpOAuthProperties mcpOAuthProperties; + + public OAuthAuthorizationServerMetadataController( + AuthorizationServerSettings settings, + McpOAuthProperties mcpOAuthProperties) { + this.settings = settings; + this.mcpOAuthProperties = mcpOAuthProperties; + } + + @GetMapping( + value = {WELL_KNOWN_PREFIX, WELL_KNOWN_PREFIX + "/**"}, + produces = MediaType.APPLICATION_JSON_VALUE) + public Map authorizationServerMetadata(HttpServletRequest request) { + String issuer = settings.getIssuer(); + + Map metadata = new LinkedHashMap<>(); + metadata.put("issuer", issuer); + metadata.put("authorization_endpoint", issuer + settings.getAuthorizationEndpoint()); + metadata.put("token_endpoint", issuer + settings.getTokenEndpoint()); + metadata.put("jwks_uri", issuer + settings.getJwkSetEndpoint()); + metadata.put("registration_endpoint", issuer + registrationPath()); + metadata.put("scopes_supported", mcpOAuthProperties.getScopes()); + metadata.put("response_types_supported", List.of("code")); + metadata.put("grant_types_supported", List.of("authorization_code", "refresh_token")); + metadata.put("token_endpoint_auth_methods_supported", List.of( + ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(), + ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue(), + ClientAuthenticationMethod.NONE.getValue() + )); + metadata.put("revocation_endpoint", issuer + settings.getTokenRevocationEndpoint()); + metadata.put("revocation_endpoint_auth_methods_supported", List.of( + ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue(), + ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue(), + ClientAuthenticationMethod.NONE.getValue() + )); + metadata.put("introspection_endpoint", issuer + settings.getTokenIntrospectionEndpoint()); + metadata.put("code_challenge_methods_supported", List.of("S256")); + + return metadata; + } + + private String registrationPath() { + String path = settings.getOidcClientRegistrationEndpoint(); + return (path != null && !path.isBlank()) ? path : "/connect/register"; + } +} diff --git a/src/main/java/com/jobtracker/config/SecurityConfig.java b/src/main/java/com/jobtracker/config/SecurityConfig.java index 51b003e..baf0875 100644 --- a/src/main/java/com/jobtracker/config/SecurityConfig.java +++ b/src/main/java/com/jobtracker/config/SecurityConfig.java @@ -81,8 +81,9 @@ public SecurityFilterChain securityFilterChain( .requestMatchers("/actuator/**").permitAll() .requestMatchers( "/.well-known/oauth-protected-resource", - "/.well-known/oauth-protected-resource/**").permitAll() - .requestMatchers("/connect/register").permitAll() + "/.well-known/oauth-protected-resource/**", + "/.well-known/oauth-authorization-server/**").permitAll() + .requestMatchers("/connect/register").denyAll() .requestMatchers("/mcp", "/mcp/**").authenticated() .requestMatchers("/api/v1/**").authenticated() .anyRequest().authenticated()) From c5bcca0ded8a2938c915df926688fccdabb42d03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 19:53:44 +0000 Subject: [PATCH 2/3] fix: restore public access to /connect/register DCR endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit added denyAll() on /connect/register in both security chains, which returned 403 for every request before it could reach the DynamicClientRegistrationController. Per RFC 7591 the DCR endpoint must be publicly accessible; abuse is already mitigated by the per-IP rate limiter in the controller itself (MAX_REGISTRATIONS_PER_MINUTE_PER_IP = 5). Changes: - SecurityConfig: denyAll() → permitAll() for /connect/register - AuthorizationServerConfig: remove dead-code denyAll() (the authorization-server matcher never matches /connect/register since it is not a built-in OAuth2 endpoint) --- .../java/com/jobtracker/config/AuthorizationServerConfig.java | 1 - src/main/java/com/jobtracker/config/SecurityConfig.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java b/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java index 33bfb39..480576e 100644 --- a/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java +++ b/src/main/java/com/jobtracker/config/AuthorizationServerConfig.java @@ -100,7 +100,6 @@ public SecurityFilterChain authorizationServerSecurityFilterChain( .securityMatcher(authServerMatcher) .authorizeHttpRequests(authorize -> authorize .requestMatchers("/login", "/default-ui.css").permitAll() - .requestMatchers("/connect/register").denyAll() .anyRequest().authenticated()) .with(authorizationServerConfigurer, authorizationServer -> authorizationServer .oidc(oidc -> oidc diff --git a/src/main/java/com/jobtracker/config/SecurityConfig.java b/src/main/java/com/jobtracker/config/SecurityConfig.java index baf0875..4c94348 100644 --- a/src/main/java/com/jobtracker/config/SecurityConfig.java +++ b/src/main/java/com/jobtracker/config/SecurityConfig.java @@ -83,7 +83,7 @@ public SecurityFilterChain securityFilterChain( "/.well-known/oauth-protected-resource", "/.well-known/oauth-protected-resource/**", "/.well-known/oauth-authorization-server/**").permitAll() - .requestMatchers("/connect/register").denyAll() + .requestMatchers("/connect/register").permitAll() .requestMatchers("/mcp", "/mcp/**").authenticated() .requestMatchers("/api/v1/**").authenticated() .anyRequest().authenticated()) From 6324dcd67e3d8422b923df841bfab00d0751f62f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:17:21 -0300 Subject: [PATCH 3/3] Changes before error encountered (#58) Agent-Logs-Url: https://github.com/vitorhugo-java/SpringBoot-JobApplyTracker/sessions/73a388a5-8f19-4858-abbe-d7a005cfca44 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .../com/jobtracker/integration/mcp/McpAuthIT.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/jobtracker/integration/mcp/McpAuthIT.java b/src/test/java/com/jobtracker/integration/mcp/McpAuthIT.java index 6904cf7..34b109f 100644 --- a/src/test/java/com/jobtracker/integration/mcp/McpAuthIT.java +++ b/src/test/java/com/jobtracker/integration/mcp/McpAuthIT.java @@ -4,8 +4,14 @@ import com.jobtracker.dto.auth.AuthResponse; import com.jobtracker.dto.auth.RegisterRequest; import com.jobtracker.integration.AbstractIntegrationTest; +import com.jobtracker.repository.ApplicationRepository; import com.jobtracker.repository.GoogleDriveConnectionRepository; +import com.jobtracker.repository.InterviewEventRepository; +import com.jobtracker.repository.PasswordResetTokenRepository; import com.jobtracker.repository.RefreshTokenRepository; +import com.jobtracker.repository.UserAchievementRepository; +import com.jobtracker.repository.UserGamificationRepository; +import com.jobtracker.repository.UserInterviewMetricsRepository; import com.jobtracker.repository.UserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -50,6 +56,12 @@ class McpAuthIT extends AbstractIntegrationTest { @Autowired private UserRepository userRepository; @Autowired private RefreshTokenRepository refreshTokenRepository; @Autowired private GoogleDriveConnectionRepository googleDriveConnectionRepository; + @Autowired private InterviewEventRepository interviewEventRepository; + @Autowired private UserInterviewMetricsRepository userInterviewMetricsRepository; + @Autowired private ApplicationRepository applicationRepository; + @Autowired private PasswordResetTokenRepository passwordResetTokenRepository; + @Autowired private UserAchievementRepository userAchievementRepository; + @Autowired private UserGamificationRepository userGamificationRepository; private String accessToken; @@ -57,6 +69,8 @@ class McpAuthIT extends AbstractIntegrationTest { void setUp() throws Exception { googleDriveConnectionRepository.deleteAll(); refreshTokenRepository.deleteAll(); + interviewEventRepository.deleteAll(); + userInterviewMetricsRepository.deleteAll(); userRepository.deleteAll(); RegisterRequest reg = new RegisterRequest("MCP Test User", "mcp-test@example.com", "pass1234", "pass1234", true);