Loading pom.xml +8 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,14 @@ <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> Loading src/main/java/org/etsi/osl/mcp/backend/AuthController.java 0 → 100644 +115 −0 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 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.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; 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; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; @GetMapping("/api/user") public Map<String, Object> getUserInfo(Authentication authentication, @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { userInfo.put("authenticated", true); userInfo.put("name", authentication.getName()); String username = authentication.getName(); if (authentication.getPrincipal() instanceof OidcUser oidcUser) { userInfo.put("email", oidcUser.getEmail()); if (oidcUser.getPreferredUsername() != null) { username = oidcUser.getPreferredUsername(); userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); } } userInfo.put("username", username); if (authorizedClient != null && authorizedClient.getAccessToken() != null) { userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); } } else { userInfo.put("authenticated", false); } return userInfo; } @PostMapping("/api/logout") public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } @GetMapping("/api/logout") public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (authentication != null) { new SecurityContextLogoutHandler().logout(request, null, authentication); } if (request.getSession(false) != null) { request.getSession().invalidate(); } String logoutUrl = buildKeycloakLogoutUrl(request, authentication); response.sendRedirect(logoutUrl); } private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { String keycloakIssuer = issuerUri; if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { if (oidcUser.getIssuer() != null) { keycloakIssuer = oidcUser.getIssuer().toString(); } } if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { return "/welcome.html"; } String baseUrl = request.getScheme() + "://" + request.getServerName(); if ((request.getScheme().equals("http") && request.getServerPort() != 80) || (request.getScheme().equals("https") && request.getServerPort() != 443)) { baseUrl += ":" + request.getServerPort(); } String postLogoutRedirectUri = baseUrl + "/welcome.html"; String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; return UriComponentsBuilder .fromUriString(keycloakLogoutUrl) .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) .build() .toUriString(); } @GetMapping("/api/health") public Map<String, String> health() { Map<String, String> response = new HashMap<>(); response.put("status", "UP"); return response; } } src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +53 −2 Original line number Diff line number Diff line Loading @@ -7,19 +7,70 @@ import org.springframework.security.config.Customizer; 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.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 { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) 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() .anyRequest().permitAll() ) // OAuth2 Login for browser-based authentication .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/", true) ) // Logout configuration .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/") .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) ) .csrf(CsrfConfigurer::disable) .build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } } src/main/resources/application.yaml +14 −4 Original line number Diff line number Diff line Loading @@ -22,16 +22,26 @@ spring: url: http://localhost:13015 security: oauth2: resourceserver: jwt: issuer-uri: http://keycloak:8080/auth/realms/openslice client: registration: authserver: keycloak: client-id: osapiWebClientId client-secret: secret authorization-grant-type: authorization_code provider: authserver redirect-uri: "{baseUrl}/login/oauth2/code/keycloak" scope: - openid - profile provider: keycloak provider: authserver: keycloak: issuer-uri: http://keycloak:8080/auth/realms/openslice authorization-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/auth token-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token user-info-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/userinfo jwk-set-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/certs activemq: brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false Loading src/main/resources/public/chat.html 0 → 100644 +60 −0 Original line number Diff line number Diff line <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OpenSlice Chat Assistant</title> <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet" href="css/chat.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/"> <img src="images/logo_clear.png" alt="OpenSlice"> </a> </div> <div class="collapse navbar-collapse" id="navbar-collapse"> <ul class="nav navbar-nav navbar-right"> <li><span class="navbar-text" id="userName"></span></li> <li><a href="#" id="logoutBtn">Logout</a></li> </ul> </div> </div> </nav> <div class="container"> <div class="chat-container"> <div class="chat-header"> <h4><span class="online-indicator"></span>OpenSlice AI Assistant</h4> <small>Ask me anything about your OpenSlice platform</small> </div> <div class="chat-messages" id="chatMessages"> <div class="message incoming"> <div class="avatar">AI</div> <div class="message-wrapper"> <div class="message-content">Hi there! How can I help you today?</div> <div class="message-time" id="initialTime"></div> </div> </div> </div> <div class="chat-input"> <div class="input-group"> <textarea class="form-control message-textarea" placeholder="Type your message..." rows="1"></textarea> <button class="btn btn-primary" type="button" id="sendBtn">Send</button> </div> </div> </div> </div> <script src="js/chat.js"></script> </body> </html> Loading
pom.xml +8 −0 Original line number Diff line number Diff line Loading @@ -51,6 +51,14 @@ <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> Loading
src/main/java/org/etsi/osl/mcp/backend/AuthController.java 0 → 100644 +115 −0 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 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.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; 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; @RestController public class AuthController { @Value("${spring.security.oauth2.client.provider.keycloak.issuer-uri:}") private String issuerUri; @GetMapping("/api/user") public Map<String, Object> getUserInfo(Authentication authentication, @RegisteredOAuth2AuthorizedClient("keycloak") OAuth2AuthorizedClient authorizedClient) { Map<String, Object> userInfo = new HashMap<>(); if (authentication != null && authentication.isAuthenticated()) { userInfo.put("authenticated", true); userInfo.put("name", authentication.getName()); String username = authentication.getName(); if (authentication.getPrincipal() instanceof OidcUser oidcUser) { userInfo.put("email", oidcUser.getEmail()); if (oidcUser.getPreferredUsername() != null) { username = oidcUser.getPreferredUsername(); userInfo.put("preferredUsername", oidcUser.getPreferredUsername()); } } userInfo.put("username", username); if (authorizedClient != null && authorizedClient.getAccessToken() != null) { userInfo.put("access_token", authorizedClient.getAccessToken().getTokenValue()); userInfo.put("token", authorizedClient.getAccessToken().getTokenValue()); } } else { userInfo.put("authenticated", false); } return userInfo; } @PostMapping("/api/logout") public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } @GetMapping("/api/logout") public void logoutGet(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { performLogout(request, response, authentication); } private void performLogout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { if (authentication != null) { new SecurityContextLogoutHandler().logout(request, null, authentication); } if (request.getSession(false) != null) { request.getSession().invalidate(); } String logoutUrl = buildKeycloakLogoutUrl(request, authentication); response.sendRedirect(logoutUrl); } private String buildKeycloakLogoutUrl(HttpServletRequest request, Authentication authentication) { String keycloakIssuer = issuerUri; if (authentication != null && authentication.getPrincipal() instanceof OidcUser oidcUser) { if (oidcUser.getIssuer() != null) { keycloakIssuer = oidcUser.getIssuer().toString(); } } if (keycloakIssuer == null || keycloakIssuer.isEmpty()) { return "/welcome.html"; } String baseUrl = request.getScheme() + "://" + request.getServerName(); if ((request.getScheme().equals("http") && request.getServerPort() != 80) || (request.getScheme().equals("https") && request.getServerPort() != 443)) { baseUrl += ":" + request.getServerPort(); } String postLogoutRedirectUri = baseUrl + "/welcome.html"; String keycloakLogoutUrl = keycloakIssuer + "/protocol/openid-connect/logout"; return UriComponentsBuilder .fromUriString(keycloakLogoutUrl) .queryParam("post_logout_redirect_uri", postLogoutRedirectUri) .build() .toUriString(); } @GetMapping("/api/health") public Map<String, String> health() { Map<String, String> response = new HashMap<>(); response.put("status", "UP"); return response; } }
src/main/java/org/etsi/osl/mcp/backend/WebSecurityConfigKeycloak.java +53 −2 Original line number Diff line number Diff line Loading @@ -7,19 +7,70 @@ import org.springframework.security.config.Customizer; 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.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 { @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) 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() .anyRequest().permitAll() ) // OAuth2 Login for browser-based authentication .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/", true) ) // Logout configuration .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/") .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) ) .csrf(CsrfConfigurer::disable) .build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthoritiesClaimName("roles"); grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } }
src/main/resources/application.yaml +14 −4 Original line number Diff line number Diff line Loading @@ -22,16 +22,26 @@ spring: url: http://localhost:13015 security: oauth2: resourceserver: jwt: issuer-uri: http://keycloak:8080/auth/realms/openslice client: registration: authserver: keycloak: client-id: osapiWebClientId client-secret: secret authorization-grant-type: authorization_code provider: authserver redirect-uri: "{baseUrl}/login/oauth2/code/keycloak" scope: - openid - profile provider: keycloak provider: authserver: keycloak: issuer-uri: http://keycloak:8080/auth/realms/openslice authorization-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/auth token-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/token user-info-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/userinfo jwk-set-uri: http://keycloak:8080/auth/realms/openslice/protocol/openid-connect/certs activemq: brokerUrl: tcp://localhost:61616?jms.watchTopicAdvisories=false Loading
src/main/resources/public/chat.html 0 → 100644 +60 −0 Original line number Diff line number Diff line <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>OpenSlice Chat Assistant</title> <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700" rel="stylesheet"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"> <link rel="stylesheet" href="css/chat.css"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script> </head> <body> <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="container-fluid"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="/"> <img src="images/logo_clear.png" alt="OpenSlice"> </a> </div> <div class="collapse navbar-collapse" id="navbar-collapse"> <ul class="nav navbar-nav navbar-right"> <li><span class="navbar-text" id="userName"></span></li> <li><a href="#" id="logoutBtn">Logout</a></li> </ul> </div> </div> </nav> <div class="container"> <div class="chat-container"> <div class="chat-header"> <h4><span class="online-indicator"></span>OpenSlice AI Assistant</h4> <small>Ask me anything about your OpenSlice platform</small> </div> <div class="chat-messages" id="chatMessages"> <div class="message incoming"> <div class="avatar">AI</div> <div class="message-wrapper"> <div class="message-content">Hi there! How can I help you today?</div> <div class="message-time" id="initialTime"></div> </div> </div> </div> <div class="chat-input"> <div class="input-group"> <textarea class="form-control message-textarea" placeholder="Type your message..." rows="1"></textarea> <button class="btn btn-primary" type="button" id="sendBtn">Send</button> </div> </div> </div> </div> <script src="js/chat.js"></script> </body> </html>