joanlofe
joanlofe

Reputation: 3657

CORS preflight request fails due to a standard header

While debugging a CORS issue I am experiencing I've found the following behaviour. Chrome makes the following OPTIONS preflight request (rewritten in CURL by Chrome itself):

curl -v 'https://www.example.com/api/v1/users' -X OPTIONS -H 'Access-Control-Request-Method: POST' -H 'Origin: http://example.com' -H 'Accept-Encoding: gzip,deflate,sdch' -H 'Accept-Language: es-ES,es;q=0.8,en;q=0.6' -H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36' -H 'Accept: */*' -H 'Referer: http://example.com/users/new' -H 'Connection: keep-alive' -H 'Access-Control-Request-Headers: accept, x-api-key, content-type'

The response from the server to this request if the following:

< HTTP/1.1 403 Forbidden
< Date: Thu, 21 Jul 2016 14:16:56 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: SAMEORIGIN
< Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
< Content-Length: 20
< Keep-Alive: timeout=5, max=100
< Connection: Keep-Alive

being the body of the response 'Invalid CORS request'. If I repeat the request removing the header 'Access-Control-Request-Method' (and only that header) the OPTIONS requests succeeds with the following reponse:

< HTTP/1.1 200 OK
< Date: Thu, 21 Jul 2016 14:21:27 GMT
* Server Apache/2.4.7 (Ubuntu) is not blacklisted
< Server: Apache/2.4.7 (Ubuntu)
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-Frame-Options: SAMEORIGIN 
< Access-Control-Allow-Headers: origin, content-type, accept, x-requested-with, x-api-key
< Access-Control-Max-Age: 60
< Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
< Access-Control-Allow-Origin: *
< Allow: GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, PATCH
< Content-Length: 0
< Keep-Alive: timeout=5, max=100
< Connection: Keep-Alive

However, the offending header is a CORS spec standard header so it should not prevent the request from succeeding, right? Why is this header causing such behaviour?

And how can I tweak the access control headers sent by my server to make the request work when made with Chrome?

By the way, I am using Chrome 36.0, and the server is using Spring Boot, with the CORS headers being managed by Spring.

When the request is made by Firefox (v47.0) the behaviour is different but with an analogue result. Firefox does not even send the preflight request, it directly sends the POST request, which receives as response a 403 Forbidden. However, if I copy the request with the 'Copy as cURL' option, and repeat it from a terminal window, It succeeds and sends the correct CORS headers in the response.

Any idea?

Update: Firefox does send the preflight OPTIONS request (as shown by the Live HTTP headers plugin), but Firebug masks it, so the behaviour in both browsers it exactly the same. In both browsers is the 'Access-control-request-method' header the difference that makes the request fail.

Upvotes: 35

Views: 96073

Answers (7)

Dexter Legaspi
Dexter Legaspi

Reputation: 3312

I came across this really while testing the CORS on our endpoints using test-cors.org website and it exhibits the exact same behavior that is described above.

The approach that I did was to use the Global CORS filter instead of using the @CrossOrigin annotation.

@Configuration
class CorsConfig : WebMvcConfigurer {
    override fun addCorsMappings(registry: CorsRegistry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("*")
                .maxAge(3600)
    }
}

Note that you should not use @EnableWebMvc unless you want to take control Spring Boot Auto-configuration as noted here...which will probably cause some "issues" as noted here and here

This next custom configuration is also needed (solution partially lifted from here) or else you will get that particular CORS pre-flight issue:

@Configuration
class CustomWebSecurityConfigurerAdapter : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.cors().and().csrf().disable()
    }
}

Upvotes: 1

Stijn Van Bael
Stijn Van Bael

Reputation: 4850

Edit: Enable CORS in security configuration and make sure options requests bypass security

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            .cors()
            .and()
            .authorizeRequests()
            .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
            // More security configuration here
}

Upvotes: 3

Prabu M
Prabu M

Reputation: 180

For me I have added @crossorigin annotation in each of controller api call.

@CrossOrigin
@PostMapping(path = "/getListOfIndividuals", produces = { "application/json" }, consumes = { "application/json" })
public ResponseEntity<String> test(@RequestBody String viewIndividualModel)
        throws Exception {
    String individualDetails = globalService.getIndividualDetails(viewIndividualModel);


    finalString = discSpecAssmentService.getViewFormForDisciplineEvaluation( viewIndividualModel);

    return new ResponseEntity<String>(finalString, HttpStatus.OK);
}

Upvotes: 3

Om Prakash Sharma
Om Prakash Sharma

Reputation: 1840

i also faced the same issue and find solution for enabling global cors issue in spring boot

@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedMethods("GET", "POST", "PUT", "DELETE").allowedOrigins("*")
                .allowedHeaders("*");
    }
}

after this , we need to enable CORS in spring security level also, so for this add cors() in your SecurityConfiguration class which extent WebSecurityConfigurerAdapter

 @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .cors()
                .and()
                .csrf().disable()
                .authorizeRequests()..

    }

Upvotes: 25

Alex Elkin
Alex Elkin

Reputation: 624

I had the same issue. I've resolve it by adding 'OPTIONS' to allowed CORS methods in my Spring MVC configuration.

@Configuration
@EnableWebMvc
@ComponentScan
public class RestApiServletConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        super.addCorsMappings(registry);
        registry.addMapping("/**")
                .allowedOrigins("http://localhost:3000", "http://localhost:8080")
                .allowedMethods("GET", "PUT", "POST", "DELETE", "OPTIONS");
    }
}

Upvotes: 8

Mike
Mike

Reputation: 904

I added this as an answer because I couldn't format it well for the top voted answer.

I found this post helpful as well: How to handle HTTP OPTIONS with Spring MVC?

DispatchServlet must be configured to pass along options request, or else it never reaches the mapped request:

...
  <servlet>
    <servlet-name>yourServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>dispatchOptionsRequest</param-name>
      <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
...

Upvotes: 1

joanlofe
joanlofe

Reputation: 3657

After a lot of struggling, I finally found the problem. I configured a request mapping in Spring to handle OPTIONS traffic, like this:

@RequestMapping(value= "/api/**", method=RequestMethod.OPTIONS)
public void corsHeaders(HttpServletResponse response) {
    response.addHeader("Access-Control-Allow-Origin", "*");
    response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
    response.addHeader("Access-Control-Allow-Headers", "origin, content-type, accept, x-requested-with");
    response.addHeader("Access-Control-Max-Age", "3600");
}

I did not know that by default Spring uses a default CORS processor, and it seems it was interfering with my request mapping. Deleting my request mapping and adding the @CrossOrigin annotation to the appropriate request mappings solved the problem.

Upvotes: 35

Related Questions