diff --git a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java index 1905527891f258e1fdcdf2d23e2e3c9983ab535f..16a4c108b98566ffe490cf83512961d0a886ff2c 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/AuthController.java +++ b/src/main/java/org/etsi/osl/mcp/backend/AuthController.java @@ -1,11 +1,14 @@ package org.etsi.osl.mcp.backend; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; @@ -13,19 +16,20 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.util.UriComponentsBuilder; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; + + @Autowired(required = false) + private OAuth2AuthorizedClientService authorizedClientService; @GetMapping("/api/user") - public Map getUserInfo(Authentication authentication, - @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { + public Map getUserInfo(Authentication authentication) { Map userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { @@ -42,9 +46,19 @@ public class AuthController { } userInfo.put("username", username); - if (authorizedClient != null && authorizedClient.getAccessToken() != null) { - userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); - userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + // Try to get OAuth2AuthorizedClient if available (from OAuth2 login) + if (authorizedClientService != null && authentication.getName() != null) { + try { + OAuth2AuthorizedClient authorizedClient = + authorizedClientService.loadAuthorizedClient("keycloak", authentication.getName()); + if (authorizedClient != null && authorizedClient.getAccessToken() != null) { + userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); + userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); + } + } catch (Exception e) { + // OAuth2 client not available - user authenticated via JWT Bearer token + // This is normal for API requests with Bearer tokens + } } } else { userInfo.put("authenticated", false); diff --git a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java index 744cd3618d1d547aa5bf6338c85129930467b1f0..51fda1a7c5cecf5b3cd748b80f5715644c493bcb 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +++ b/src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java @@ -1,40 +1,66 @@ package org.etsi.osl.mcp.backend; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -import org.springframework.security.config.Customizer; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; -import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.http.HttpStatus; @Configuration @EnableWebSecurity @Profile("!testing") public class WebSecurityConfigKeycloak { + @Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}") + private String issuerUri; + + // Security filter chain for JWT Bearer token only (API endpoints) + @Bean + @Order(1) + SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception { + return http + .securityMatcher("/ask/**") + .authorizeHttpRequests(auth -> auth + .anyRequest().authenticated() + ) + .oauth2ResourceServer(oauth2 -> oauth2 + .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .exceptionHandling(exception -> exception + .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) + ) + .csrf(CsrfConfigurer::disable) + .build(); + } + + // Security filter chain for OAuth2 login (UI) @Bean - SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + @Order(2) + SecurityFilterChain webSecurityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth // Public static resources .requestMatchers("/", "/index.html", "/styles.css", "/robot.svg", "/favicon.ico").permitAll() .requestMatchers("/api/health").permitAll() - .requestMatchers("/api/user").permitAll() - .requestMatchers("/api/logout").permitAll() - .requestMatchers("/logout").permitAll() - .requestMatchers("/ask", "/api/**").authenticated() + // UI-only endpoints (OAuth2 login) + .requestMatchers("/api/user", "/api/logout", "/logout").permitAll() .anyRequest().permitAll() ) - // OAuth2 Login for browser-based authentication + // OAuth2 Login for browser-based authentication (UI) .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/", true) ) @@ -45,17 +71,6 @@ public class WebSecurityConfigKeycloak { .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ) - .oauth2Client(Customizer.withDefaults()) - .oauth2ResourceServer(oauth2 -> oauth2 - .jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())) - .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)) - ) - .exceptionHandling(exception -> exception - .defaultAuthenticationEntryPointFor( - new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new AntPathRequestMatcher("/ask") - ) - ) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) @@ -73,4 +88,11 @@ public class WebSecurityConfigKeycloak { jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } + + @Bean + public JwtDecoder jwtDecoder() { + // Eagerly initialize the JWT decoder on application startup + // This fetches the JWKS from Keycloak immediately + return NimbusJwtDecoder.withJwkSetUri(issuerUri + "/protocol/openid-connect/certs").build(); + } } diff --git a/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java index d446dc1b31445c5a043acd9a94e0f9d859f246ff..87ca06c06c60485a16289fea35de1ea306103b84 100644 --- a/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +++ b/src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java @@ -1,13 +1,6 @@ package org.etsi.osl.mcp.backend.configuration; -import io.modelcontextprotocol.client.transport.customizer.McpSyncHttpClientRequestCustomizer; -import org.springaicommunity.mcp.security.client.sync.AuthenticationMcpTransportContextProvider; -import org.springaicommunity.mcp.security.client.sync.oauth2.http.client.OAuth2AuthorizationCodeSyncHttpRequestCustomizer; - -import org.springframework.ai.mcp.customizer.McpSyncClientCustomizer; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; @@ -17,17 +10,20 @@ import org.springframework.security.oauth2.client.registration.InMemoryClientReg @Configuration class McpConfiguration { - @Bean - McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, - ClientRegistrationRepository clientRegistrationRepository) { - var registrationId = findUniqueClientRegistration(clientRegistrationRepository); - return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); - } + // Disabled OAuth2 customization for MCP client - not needed for JWT-based API + // The MCP server connection doesn't need OAuth2 authentication + + // @Bean + // McpSyncHttpClientRequestCustomizer requestCustomizer(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager, + // ClientRegistrationRepository clientRegistrationRepository) { + // var registrationId = findUniqueClientRegistration(clientRegistrationRepository); + // return new OAuth2AuthorizationCodeSyncHttpRequestCustomizer(oAuth2AuthorizedClientManager, registrationId); + // } - @Bean - McpSyncClientCustomizer syncClientCustomizer() { - return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); - } + // @Bean + // McpSyncClientCustomizer syncClientCustomizer() { + // return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); + // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if