SecurityConfig.java

package com.MedilaboSolutions.gateway.security;

import com.MedilaboSolutions.gateway.filters.AuthFilter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.ResponseCookie;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.logout.SecurityContextServerLogoutHandler;
import org.springframework.web.cors.CorsConfiguration;

import java.time.Duration;
import java.util.List;

@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebFluxSecurity
public class SecurityConfig {

    private final AuthFilter authFilter;
    private final UnauthorizedEntryPoint unauthorizedEntryPoint;
    private final OAuth2SuccessHandler oauth2SuccessHandler;
    private final OAuth2FailureHandler oauth2FailureHandler;
    private final UserDetailsServiceImpl userDetailsServiceImpl;

    SecurityContextServerLogoutHandler securityContextLogoutHandler = new SecurityContextServerLogoutHandler();

    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {

        return http
                .csrf(ServerHttpSecurity.CsrfSpec::disable)
                // CORS configuration to allow requests from the Vue.js frontend running on localhost during development
                // This setup enables cross-origin requests with credentials (headers)
                .cors(cors -> cors.configurationSource(request -> {
                    CorsConfiguration config = new CorsConfiguration();
                    config.setAllowCredentials(true);
                    config.setAllowedOrigins(List.of("https://localhost:5173"));
                    config.setAllowedHeaders(List.of("*"));
                    config.setAllowedMethods(List.of("GET", "POST", "PUT", "OPTIONS"));
                    return config;
                }))
                // When an unauthenticated user tries to access a protected resource,
                // delegate the response to the UnauthorizedEntryPoint to return a 401 Unauthorized status.
                .exceptionHandling(e -> e.authenticationEntryPoint(unauthorizedEntryPoint))
                // Allow healthcheck for Docker; avoid exposing actuator endpoints in prod
                .authorizeExchange(ex -> ex
                        // Only the user with ROLE_MEDECIN can access their own user info
                        // Prevents exposing sensitive user data (even if, there is none if no one is logged in)
                        .pathMatchers("/oauth2/user").hasRole("MEDECIN")
                        .pathMatchers(
                                "/login",
                                // OAuth2 callback URL where Spring Security receives the authorization code, exchanges
                                // it for tokens, and builds the authenticated user details with the user attributes
                                "/login/oauth2/code/**",
                                "/oauth2/**",
                                "/refresh",
                                "/logout",
                                "/actuator/**"
                        ).permitAll()
                        .anyExchange().hasRole("MEDECIN")
                )
                .oauth2Login(oauth2 -> oauth2
                        .authenticationSuccessHandler(oauth2SuccessHandler)
                        .authenticationFailureHandler(oauth2FailureHandler)
                )
                .logout(logout -> logout
                        .logoutUrl("/logout")
                        .logoutHandler(securityContextLogoutHandler)
                        .logoutSuccessHandler((webFilterExchange, authentication) -> {

                            log.info("Logout detected, clearing refreshToken cookie for user: {}",
                                    (authentication != null) ? authentication.getName() : "anonymous");
                            ResponseCookie clearCookie = ResponseCookie.from("refreshToken", "")
                                    .httpOnly(true)
                                    .secure(true)
                                    .sameSite("Strict")
                                    .maxAge(Duration.ZERO)
                                    .path("/")
                                    .build();
                            webFilterExchange.getExchange().getResponse().addCookie(clearCookie);

                            return webFilterExchange.getExchange().getResponse().setComplete();
                        })
                )
                // Adds the custom JWT auth filter BEFORE Spring Security’s default authentication processing
                .addFilterBefore(authFilter, SecurityWebFiltersOrder.AUTHENTICATION)
                // Disable HTTP Basic auth (no browser popup)
                .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
                // Disable form login (no login page)
                .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
                .build();
    }

    @Bean
    public ReactiveUserDetailsService reactiveUserDetailsService() {
        return userDetailsServiceImpl;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

}