Search code examples
angularspring-bootcorsspring-webfluxspring-webclient

Spring Security CORS with Angular 16


First of all my setup. I am using Spring-boot 3.1 with spring security. The api is the spring boot application while Angular is the client application both running on different hosts. Spring boot runs on host http://api.example.com while angular runs on http://client.example.com.

This is my configuration for CORS at the moment.

@Configuration
@EnableWebSecurity 
public class SecurityConfig {

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://client.example.com"));
    
        // tried this too but without any luck.
        // configuration.setAllowedOrigins(Arrays.asList("localhost:4200"));

        configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

        return httpSecurity.build();
    }
}

AS from the spring documentation CORS has to be configured first.

I have two kinds of requests one that fetches resources from the database(this one passes cors with the above configuration). And one that uses WebClient to make a request to keycloak to perform the login operation(this one is failing due to CORS).

By now i understand or i think i understand that WebFlux uses Webclient. That means that i have to configure security for webflux???

And this is where i am loosing it. I tried the bellow configuration but without any luck.

@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
        .allowedOrigins("http://client.example.com")
        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
        .allowCredentials(true);
    }
}

console error Access to XMLHttpRequest at 'http://api.example.com/login' from origin 'http://client.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Any suggestions?


Solution

  • I finally manage to get through this.

    My first assumption that it was a problem coming from the internal request to keycloak was wrong. I understood that when i created another dummy post request. I had the same issue.

    Digging through tons of answers on the web and reading again and again the documentation there was one option i did not follow. The CorsFilter.

    After implementing the CorsFilter thought i had the same issue....then just by pure luck i found an issue with my nginx configuration. When i fixed that CorsFilter work like a charm form me. So this is the full implementation hope that will help someone in the future.

    One more time my set up:

    Spring-boot 3.1 with spring-security enabled this is all that maters at the time.

    SecurityConfig.java

    // other security configuration
    
    ....    
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            .addFilterBefore(new CorsFilter(), ChannelProcessingFilter.class)
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .requestMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
            )
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
    
        return httpSecurity.build();
    }
    

    CorsFilter.java

    public class CorsFilter implements Filter {
    private final Logger log = LoggerFactory.getLogger(CorsFilter.class);
    
    public CorsFilter() {
        log.info("CorsFilter Init");
    }
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
    
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
    
        response.setHeader("Access-Control-Allow-Origin", "http://www.your-domain.me");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Content-Type");
    
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_ACCEPTED);
            return;
        }
    
        chain.doFilter(req, res);
    }
    
    @Override
    public void init(FilterConfig filterConfig) {}
    
    @Override
    public void destroy() {}
    }
    

    This is the configuration that worked for me. Enjoy!