Commit f550c480 authored by Irene Denazi's avatar Irene Denazi
Browse files

BE Service #2: Implement authorization BE Service controller and Chat UI

parent 5922d220
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -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>
+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;
    }
}
+53 −2
Original line number Diff line number Diff line
@@ -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;
  }
}
+14 −4
Original line number Diff line number Diff line
@@ -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
+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