package com.mailserver.config; import com.mailserver.security.JwtAuthenticationFilter; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.context.annotation.Configuration; 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.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsConfigurationSource; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; @Configuration @EnableWebFluxSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final String corsOrigins; public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, @org.springframework.beans.factory.annotation.Value("${mailserver.cors.origins:}") String corsOrigins) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.corsOrigins = corsOrigins; } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // The web client is served from the same origin; other origins only for development (ng serve) configuration.setAllowedOrigins(java.util.Arrays.stream(corsOrigins.split(",")).map(String::trim).filter(o -> !o.isEmpty()).toList()); configuration.setAllowedMethods(java.util.Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); configuration.setAllowedHeaders(java.util.Arrays.asList("Authorization", "Content-Type", "Accept", "X-Requested-With")); configuration.setAllowCredentials(true); configuration.setExposedHeaders(java.util.Arrays.asList("Authorization")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", configuration); return source; } @Bean public SecurityWebFilterChain filterChain(ServerHttpSecurity http) { http .csrf(ServerHttpSecurity.CsrfSpec::disable) // JWT only: without this an API call with no token gets 401 + "WWW-Authenticate: Basic", // and the browser shows its own login popup where our mailbox password does not work .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable) .exceptionHandling(e -> e.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED))) .cors(cors -> cors.configurationSource(corsConfigurationSource())) .authorizeExchange(exchange -> exchange .pathMatchers("/api/login", "/api/register", "/api/config", "/actuator/health").permitAll() .pathMatchers("/api/**", "/actuator/**").authenticated() // the web client (static files, SPA routes) .anyExchange().permitAll() ) .addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); return http.build(); } }