Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confused about how to handle CORS OPTIONS preflight requests

I'm new to working with Cross Origin Resource Sharing and trying to get my webapp to respond to CORS requests. My webapp is a Spring 3.2 app running on Tomcat 7.0.42.

In my webapp's web.xml, I have enabled the Tomcat CORS filter:

<!-- Enable CORS (cross origin resource sharing) -->
<!-- http://tomcat.apache.org/tomcat-7.0-doc/config/filter.html#CORS_Filter -->
<filter>
  <filter-name>CorsFilter</filter-name>
  <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>CorsFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>   

My client (written with AngularJS 1.2.12) is trying to access a REST endpoint with Basic Authentication enabled. When it makes it's GET request, Chrome is first preflighting the request, but is receiving a 403 Forbidden response from the server:

Request URL:http://dev.mydomain.com/joeV2/users/listUsers
Request Method:OPTIONS
Status Code:403 Forbidden
Request Headers:
   OPTIONS /joeV2/users/listUsers HTTP/1.1
   Host: dev.mydomain.com
   Connection: keep-alive
   Cache-Control: max-age=0
   Access-Control-Request-Method: GET
   Origin: http://localhost:8000
   User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107 Safari/537.36
   Access-Control-Request-Headers: accept, authorization
   Accept: */*
   Referer: http://localhost:8000/
   Accept-Encoding: gzip,deflate,sdch
   Accept-Language: en-US,en;q=0.8
Response Headers:
   HTTP/1.1 403 Forbidden
   Date: Sat, 15 Feb 2014 02:16:05 GMT
   Content-Type: text/plain; charset=UTF-8
   Content-Length: 0
   Connection: close

I'm not entirely sure how to proceed. The Tomcat filter, by default, accepts the OPTIONS header to access the resource.

The problem, I believe, is that my resource (the request URL) http://dev.mydomain.com/joeV2/users/listUsers is configured to only accept GET methods:

@RequestMapping( method=RequestMethod.GET, value="listUsers", produces=MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public List<User> list(){
    return userService.findAllUsers();
}

Does this mean that I must make that method/endpoint accept OPTIONS method as well? If so, does that mean I have to explicitly make every REST endpoint accept the OPTIONS method? Apart from cluttering code, I'm confused how that would even work. From what I understand the OPTIONS preflight is for the browser to validate that the browser should have access to the specified resource. Which I understand to mean that my controller method should not even be called during the preflight. So specifying OPTIONS as an accepted method would be counter-productive.

Should Tomcat be responding to the OPTIONS request directly without even accessing my code? If so, is there something missing in my configuration?

like image 925
Eric B. Avatar asked Feb 15 '14 02:02

Eric B.


People also ask

How does a CORS preflight work?

CORS also relies on a mechanism by which browsers make a "preflight" request to the server hosting the cross-origin resource, in order to check that the server will permit the actual request. In that preflight, the browser sends headers that indicate the HTTP method and headers that will be used in the actual request.

What HTTP method is used for a preflight CORS request?

A CORS preflight request is a CORS request that checks to see if the CORS protocol is understood and a server is aware using specific methods and headers. It is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method , Access-Control-Request-Headers , and the Origin header.

How do I skip a preflight request?

Preflight is a web security feature implemented by the browser. For Chrome you can disable all web security by adding the --disable-web-security flag. For example: "C:\Program Files\Google\Chrome\Application\chrome.exe" --disable-web-security --user-data-dir="C:\newChromeSettingsWithoutSecurity" .


2 Answers

I sat down and debugged through the org.apache.catalina.filters.CorsFilter to figure out why the request was being forbidden. Hopefully this can help someone out in the future.

According to the W3 CORS Spec Section 6.2 Preflight Requests, the preflight must reject the request if any header submitted does not match the allowed headers.

The default configuration for the CorsFilter cors.allowed.headers (as is yours) does not include the Authorization header that is submitted with the request.

I updated the cors.allowed.headers filter setting to accept the authorization header and the preflight request is now successful.

<filter>   <filter-name>CorsFilter</filter-name>   <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>     <init-param>         <param-name>cors.allowed.headers</param-name>         <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization</param-value>     </init-param>      </filter> 

Of course, I'm not sure why the authorization header is not by default allowed by the CORS filter.

like image 146
Eric B. Avatar answered Oct 21 '22 09:10

Eric B.


The first thing I would try is to set your common headers for your http requests that angular dispatches, by inserting the following config block on your module:

.config(function($httpProvider){     $httpProvider.defaults.headers.common = {};     $httpProvider.defaults.headers.post = {};     $httpProvider.defaults.headers.put = {};     $httpProvider.defaults.headers.patch = {}; }) 

These are supposed to be set by default already, but I've found that I often have to do this manually in my config due to any overrides from other modules or internal angular bootstraping process.

Your CORS filter should be enough on the server side to allow these types of requests, but sometimes, you need to specify request methods in addition to your origins, as well as accepted content types. The tomcat docs have this advanced block, which addresses those.

 <filter>   <filter-name>CorsFilter</filter-name>   <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>   <init-param>     <param-name>cors.allowed.origins</param-name>     <param-value>*</param-value>   </init-param>   <init-param>     <param-name>cors.allowed.methods</param-name>     <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value>   </init-param>   <init-param>     <param-name>cors.allowed.headers</param-name>     <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>   </init-param>   <init-param>     <param-name>cors.exposed.headers</param-name>     <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>   </init-param>   <init-param>     <param-name>cors.support.credentials</param-name>     <param-value>true</param-value>   </init-param>   <init-param>     <param-name>cors.preflight.maxage</param-name>     <param-value>10</param-value>   </init-param> </filter> <filter-mapping>   <filter-name>CorsFilter</filter-name>   <url-pattern>/*</url-pattern> </filter-mapping> 

If the first doesn't work on it's own, try enhancing your filters, especially:

<init-param>         <param-name>cors.allowed.methods</param-name>         <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value>       </init-param>       <init-param>         <param-name>cors.allowed.headers</param-name>         <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value>       </init-param>       <init-param>         <param-name>cors.exposed.headers</param-name>         <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value>       </init-param> 
like image 20
Brian Vanderbusch Avatar answered Oct 21 '22 09:10

Brian Vanderbusch