Commit 0ba902cb authored by Irene Denazi's avatar Irene Denazi
Browse files

Issue #6: Fix Authorization for BE Service

parent aeb03f41
Loading
Loading
Loading
Loading
Loading
+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

@@ -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"]
+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;
@@ -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 {
@@ -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()) {
@@ -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);
        }
+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)
          )
@@ -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();
  }
}
+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;

@@ -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