Loading Dockerfile +22 −4 Original line number Diff line number Diff line FROM eclipse-temurin:17-jre-alpine # Build stage FROM maven:3.9-eclipse-temurin-17 AS build MAINTAINER osl.etsi.org # Set working directory WORKDIR /app # Copy pom.xml and download dependencies COPY pom.xml . RUN mvn dependency:go-offline -B || true # Copy source code COPY src ./src # Build the application RUN mvn clean package -DskipTests -B # Runtime stage FROM eclipse-temurin:17-jre-alpine MAINTAINER osl.etsi.org # Create application directory RUN mkdir -p /opt/openslice/lib/ # Copy the built JAR from a previous pipeline COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib # Copy the built JAR from build stage COPY --from=build /app/target/org.etsi.osl.mcp.backend-*.jar /opt/openslice/lib/org.etsi.osl.mcp.backend.jar # Expose port EXPOSE 11880 Loading @@ -15,4 +33,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend.jar"] src/main/java/org/etsi/osl/mcp/backend/AuthController.java +25 −11 Original line number Diff line number Diff line 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; Loading @@ -13,9 +16,8 @@ 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 { Loading @@ -23,9 +25,11 @@ 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<String, Object> getUserInfo(Authentication authentication, @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { public Map<String, Object> getUserInfo(Authentication authentication) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { Loading @@ -42,10 +46,20 @@ public class AuthController { } userInfo.put("username", username); // 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); } Loading src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +43 −21 Original line number Diff line number Diff line 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 SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @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 @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) ) Loading @@ -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) ) Loading @@ -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(); } } src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +13 −17 Original line number Diff line number Diff line 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; Loading @@ -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 McpSyncClientCustomizer syncClientCustomizer() { return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); } // @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()); // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if Loading Loading
Dockerfile +22 −4 Original line number Diff line number Diff line FROM eclipse-temurin:17-jre-alpine # Build stage FROM maven:3.9-eclipse-temurin-17 AS build MAINTAINER osl.etsi.org # Set working directory WORKDIR /app # Copy pom.xml and download dependencies COPY pom.xml . RUN mvn dependency:go-offline -B || true # Copy source code COPY src ./src # Build the application RUN mvn clean package -DskipTests -B # Runtime stage FROM eclipse-temurin:17-jre-alpine MAINTAINER osl.etsi.org # Create application directory RUN mkdir -p /opt/openslice/lib/ # Copy the built JAR from a previous pipeline COPY /target/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar /opt/openslice/lib # Copy the built JAR from build stage COPY --from=build /app/target/org.etsi.osl.mcp.backend-*.jar /opt/openslice/lib/org.etsi.osl.mcp.backend.jar # Expose port EXPOSE 11880 Loading @@ -15,4 +33,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:11880/actuator/health || exit 1 # Run application CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend-1.0.0-SNAPSHOT.jar"] CMD ["java", "-jar", "/opt/openslice/lib/org.etsi.osl.mcp.backend.jar"]
src/main/java/org/etsi/osl/mcp/backend/AuthController.java +25 −11 Original line number Diff line number Diff line 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; Loading @@ -13,9 +16,8 @@ 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 { Loading @@ -23,9 +25,11 @@ 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<String, Object> getUserInfo(Authentication authentication, @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { public Map<String, Object> getUserInfo(Authentication authentication) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { Loading @@ -42,10 +46,20 @@ public class AuthController { } userInfo.put("username", username); // 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); } Loading
src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +43 −21 Original line number Diff line number Diff line 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 SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { @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 @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) ) Loading @@ -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) ) Loading @@ -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(); } }
src/main/java/org/etsi/osl/mcp/backend/configuration/McpConfiguration.java +13 −17 Original line number Diff line number Diff line 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; Loading @@ -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 McpSyncClientCustomizer syncClientCustomizer() { return (name, syncSpec) -> syncSpec.transportContextProvider(new AuthenticationMcpTransportContextProvider()); } // @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()); // } /** * Returns the ID of the {@code spring.security.oauth2.client.registration}, if Loading