26

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?

Eric B.
  • 20,257
  • 43
  • 147
  • 269

2 Answers2

48

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.

Eric B.
  • 20,257
  • 43
  • 147
  • 269
  • curious if you tried the first bit off my answer? I was using apache, and didn't need to set the `authorization` header. Check your `$httpProvider` settings for the Access-Control-Request-Headers settings. Your get request is expecting that `authorization` header acceptance. configuring your `$httpProvider` with `$httpProvider.defaults.headers.common = {}` *should* reset that. (which is why I was wondering if you tried it). I prefer to have the most applicable Tomcat settings I can as a security rule. being to generic with your CORS settings can open you up to problems down the road. – Brian Vanderbusch Feb 15 '14 at 04:41
  • @BrianVanderbusch No - I actually hadn't gotten your response until I had finished debugging TC and figured out where the issue was. I will test with your proposed solution as well, although I'm not entirely sure if that will matter considering the `authorization` header is what was triggering the issue. In my case, I _need_ the `authorization` header as it is a REST endpoint to which I need to authenticate for each request. – Eric B. Feb 17 '14 at 03:17
  • 1
    @Eric A little bit late here, but the `Authorization` header is actually not supposed to accompany preflight requests, according to the spec [here](http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0). The point of the preflight is purely to ensure that the method (GET in your case) and the headers are valid to send to the server. No authorization/authentication should happen – Matt Dodge May 31 '14 at 00:15
  • @mattedgod Does that mean its a browser issue if it is sending the Auth header along with the preflight request? I'm using Chrome. – Eric B. Jun 02 '14 at 02:32
  • @EricB. I'm surprised Chrome would be doing that, but my understanding was that those preflight requests were handled by the browser and not the library making the request. In other words, jQuery won't have to handle making a preflight request, it trusts the browser to do that. However, on my version of Chrome (35) on OS X no Auth header gets passed. Could it possibly be because your `authorization` is lower case? Seems odd but I have no other explanation – Matt Dodge Jun 02 '14 at 16:46
  • My CORS preflight OPTIONS request is met by the server with a 401 Unauthorized stopping dead the CORS hand shake. – Stephane Aug 20 '14 at 21:40
  • @StephaneEybert What is doing the authentication on your side? Your application, or the server itself? I'm using Tomcat, but doing authentication by my application. Tomcat's CorsFilter runs before the app is even reached so authentication is not an issue. – Eric B. Aug 21 '14 at 00:12
  • Hi, sorry, in bed now, 2am past here, I made a custom CORS filter called by an http before... in my Spring security configuration. – Stephane Aug 21 '14 at 00:25
  • I will publish more tomorrow, on smartphone now. – Stephane Aug 21 '14 at 00:26
  • Hi Eric, I have a custom filter: public class SimpleCORSFilter implements Filter {} And I'm trying these three alternatives. Either make use of the @WebFilter(urlPatterns = "/*") annotation only, OR make use of the @Component annotation coupled with the following in my security configuration: http.addFilterBefore(simpleCORSFilter, ChannelProcessingFilter.class); OR http.authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll(); But the trouble is that the 401 Unauthorized is coming only from Chrome and Firefox and not every time either. – Stephane Aug 21 '14 at 06:46
  • @Eric The first option with the @WebFilter(urlPatterns = "/*") gives the 401 Unauthorized on the OPTIONS request of the CORS handshake on Chrome. – Stephane Aug 21 '14 at 06:48
  • @Eric The first plus the third options coupled together also gives the Unauthorized on the OPTIONS request of the CORS handshake on Chrome and Firefox. – Stephane Aug 21 '14 at 07:03
  • The second option prevents the Unauthorized on the OPTIONS request of the CORS handshake on Chrome and Firefox. So it seems to be the one to use: the @Component annotation coupled with the http.addFilterBefore(simpleCORSFilter, ChannelProcessingFilter.class); injection. – Stephane Aug 21 '14 at 07:05
  • Even a bit more later, I'm having this issue and I'm using Chrome and I'm not seeing it sending an `Authorization` header. He's sending an `Access-Control-Request-Headers` with the value `authorization` during the preflight request, but to me that's not the same as sending an `Authorization` header. By the way, I tried your recommendation but it didn't work for me... – Pere Mar 08 '17 at 07:34
  • @pere. This was a couple of years ago so I don't remember the exact details, but changing my CorsFilter parameter to accept the appropriate headers was what I needed. It may be that the filter config had changed since my post or depending on your app you may need to configure it different. – Eric B. Mar 08 '17 at 12:17
4

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>
Brian Vanderbusch
  • 3,221
  • 5
  • 29
  • 43