Dhruva Cloud
← All articles

Fix Missing CORS Headers on Spring Security Endpoints

03 Nov 2026 · 4 min read · by Dhruva Cloud

Developers configuring Cross-Origin Resource Sharing (CORS) in Spring Boot often find that standard controllers return the correct access headers, but endpoints managed by Spring Security—such as /login or /logout—omit them completely. When a browser client attempts a preflight OPTIONS request or cross-origin authentication call, it fails with the browser console error Access to XMLHttpRequest at 'http://example.com/login' from origin 'http://example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. This happens because Spring Security filters intercept and process requests before standard Spring MVC CORS configurations execute.

Quick fix

  1. Define a CorsConfigurationSource bean that explicitly specifies allowed origins, methods, and headers.
  2. Inject that CorsConfigurationSource into your Spring Security filter chain using http.cors(cors -> cors.configurationSource(...)).
  3. Ensure Spring Security permits unauthenticated preflight OPTIONS requests or allows the request to pass through the security filter chain.
  4. Restart your application and clear your browser cache before testing.

Why this happens

How to fix it

Fixing CORS in Modern Spring Boot (Spring Boot 3+ / Spring Security 6+)

In modern Spring Security, Security configurations use a SecurityFilterChain bean instead of extending legacy base classes. You must wire a CorsConfigurationSource directly into the Security DSL so that Spring Security places the CorsFilter at the highest priority in its security chain.

Update your security configuration class (e.g., SecurityConfig.java) with the following structure:

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.Customizer;

import java.util.List;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // Enable CORS and integrate with the CorsConfigurationSource bean below
            .cors(Customizer.withDefaults())
            .csrf(csrf -> csrf.disable()) // Configure CSRF according to your auth model
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/login", "/public/**").permitAll()
                .anyRequest().authenticated()
            );

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        
        // Define origins allowed to make requests
        configuration.setAllowedOrigins(List.of("https://example.com", "http://localhost:3000"));
        
        // Specify allowed HTTP methods
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        
        // Specify allowed request headers
        configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
        
        // Allow credentials like cookies or HTTP authentication headers
        configuration.setAllowCredentials(true);
        
        // Expose headers if your frontend needs to read custom response headers
        configuration.setExposedHeaders(List.of("Authorization", "Location"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // Apply this CORS configuration to all paths in the application
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Enforcing CorsFilter Priority via Filter Registration

If you have custom security filters executing before authentication, register a CorsFilter explicitly with high precedence so that CORS response headers are attached even if downstream security filters throw an authentication exception.

package com.example.demo.config;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.List;

@Configuration
public class PriorityCorsConfig {

    @Bean
    public FilterRegistrationBean<CorsFilter> customCorsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        
        config.setAllowCredentials(true);
        config.setAllowedOrigins(List.of("https://example.com"));
        config.setAllowedHeaders(List.of("*"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
        
        source.registerCorsConfiguration("/**", config);
        
        FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
        // Force the filter to execute before any security filters
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

Check that it worked

Verify that your security endpoints respond correctly to cross-origin preflight requests using curl from your web terminal or local console. Execute an HTTP OPTIONS request targeted specifically at your authentication URL:

curl -i -X OPTIONS https://example.com/login \
  -H "Origin: https://example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Authorization, Content-Type"

Examine the output headers. A successful fix returns an HTTP/1.1 200 OK or 204 No Content status with the following response headers present:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true

Prevent it next time

FAQ

Why does @CrossOrigin work on regular controllers but fail on /login?

The @CrossOrigin annotation is handled by Spring MVC's HandlerMapping inside the DispatcherServlet. Endpoints generated by Spring Security filters (such as standard form login or OAuth endpoints) are intercepted before reaching the DispatcherServlet, rendering controller-level annotations ineffective.

Can I just allow all origins using "*"?

If your application sends credentials (such as cookies or HTTP Authorization headers), browsers explicitly reject the wildcard * in Access-Control-Allow-Origin response headers. You must specify exact origins or use origin patterns.

Should I handle CORS at the Nginx host layer or in Spring Boot?

Handling CORS inside Spring Boot ensures consistent application logic across development and production environments. However, if you run a reverse proxy like Nginx in front, ensure only one layer handles CORS to avoid duplicated origin headers.

Spring Apps on Dhruva Cloud

When running Java and Spring Boot applications, deploying behind an automated edge architecture simplifies network routing. Dhruva Cloud uses Cloudflare CDN and anycast DDoS protection in front of every environment, routing requests through secure encrypted tunnels straight to your application process.

Looking for a fast, secure home for your website? Dhruva Cloud hosts it on your own domain with free SSL, Cloudflare’s global CDN and DDoS protection on every plan. See the plans →

Keep reading