'Spring Security Rejects Fetch OPTIONS Preflight Outright If content-type = 'application/json'
If a Fetch POST to a Spring Security (v 5.6.1) enabled service endpoint sends this header:
headers.append("Content-Type", "application/json");
the OPTIONS preflight request will not be handled by any filter in the filter chain - filter logging shows no response whatsoever from any chain filter; there isn't even a server-side invocation of org.apache.catalina.connector.RequestFacade
in response to the preflight call. The HttpResponse the client receives will always just show 403 Unauthorized
.
So it isn't that this kind of request causes a preflight, which would then propagate through the filter chain and be handled by an enabled CorsFilter which would add the requisite headers to satisfy the preflight and return this response. It's that the preflight is apparently simply rejected outright if application/json is the content-type. No service response to it at all.
This situation exists with this config in place:
@Configuration
@EnableWebSecurity(debug = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurity extends WebSecurityConfigurerAdapter {
private Environment environment;
private UserService service;
final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
public WebSecurity(Environment environment,
UserService service) {
this.environment = environment;
this.service = service;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(new CustomCorsFilter(), UsernamePasswordAuthenticationFilter.class);
http.csrf().disable();
http.authorizeRequests()
.antMatchers("/users/**")
.hasIpAddress(environment.getProperty("gateway.ip"));
http.headers().frameOptions().disable();
}
}
CustomCorsConfig:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomCorsFilter extends CorsFilter {
final Logger logger = LoggerFactory.getLogger(getClass());
public CustomCorsFilter() {
super(configurationSource());
}
private static UrlBasedCorsConfigurationSource configurationSource() {
List<String> allHeaders = Arrays.asList("X-Auth-Token",
"Content-Type",
"X-Requested-With",
"XMLHttpRequest",
"Accept",
"Key",
"Authorization",
"X-Authorization");
List<String> allowedMethods = Arrays.asList("GET","POST","OPTIONS");
List<String> allowedOrigins = Arrays.asList("http://localhost:3000", "http://localhost:8082");
CorsConfiguration corsConfig = new CorsConfiguration();
corsConfig.setAllowedHeaders(allHeaders);
corsConfig.setAllowedMethods(allowedMethods);
corsConfig.setAllowedOrigins(allowedOrigins);
corsConfig.setExposedHeaders(allHeaders);
corsConfig.setMaxAge(3600L);
corsConfig.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "http://localhost:3000");
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, OPTIONS");
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, Content-Type, Accept");
Object[] headerNames = response.getHeaderNames().toArray();
String names = "";
for (Object o : headerNames) {
String s = (String)o;
names += s + ", ";
}
logger.info("\n ** CustomCorsFilter.doFilterInternal(): header names are: " + names + "\n\n");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
protected boolean shouldNotFilterErrorDispatch() {
return false;
}
}
Spring Security runs only via the filter chain. If it's in use, the @CrossOrigin
controller annotation will be irrelevant (won't be applied) since the filter chain filters will execute before the request gets to the controller endpoint.
The only way I found to get the request handled by the SpringBoot (v 2.6.2) service at all was to set the header as:
headers.append("Content-Type", "text/plain");
But this of course means that any/all service endpoints can't consume = application/json
, since being called from a fetch client specifying content-type = application/json
they would all be subject to an OPTIONS preflight.
This can't be the situation Spring Security envisioned; Fetch-to-SpringSecurity Microservice is a very prevalent implementation. Also, if comsumes = text/plain
none of Spring's out-of-box HttpMessageConverters successfully map the endpoint's specified @RequestBody
to a specified domain POJO type - trying this causes
[org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'text/plain;charset=UTF-8' not supported]
Please let me know what I'm missing here. Also, is there a way to pose this issue directly to Spring Security dev?
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|