Sunday, April 21, 2013

CORS: Doing the preflight dance with Jetty and Angularjs

Foreword

After giving my best with CORS I'm finally giving up out of following reasons:

  • Browsers behave differently with CORS. This is not usable. One can assume what happens when the next browser upgrade is coming. Debugging, debugging, debugging which just isn't worth the time.
  • Setting Authorization: Basic headers will simply not work. In the W3C specs they have a section for credentials, but they don't loose a word about setting the Authorization header. The only thing that is for sure is that it will be a pre-flight request which other developers warn from using.
  • I have spent about 3 hours every day within 2 weeks to find a way to use CORS with Authorization headers... no luck
  • After that I needed a big mug of beer :) With big mug I mean the equivalent to two bottles of white wine...

Even after these warning signs I believed in my own stupidity until I fell over following sentences:

Funnily enough, when making a CORS request using jQuery, the JavaScript library specifically avoids setting the custom header, along with a word of warning to developers:

// For cross-domain requests, seeing as conditions for a preflight are akin to a jigsaw puzzle, we simply never set it to be sure.

So possibly the best advice, if possible, is to avoid setting the custom header if you don’t want to do the preflight dance.

Otherwise good luck my friend, I hope this has helped!

This boosted my self confidence again seeing that also other people went to the asylum :) But I also found it a petty that I can't use Basic Authentication for cross domain requests. So my next plan is to try solving the issue with tokens or simply not using cross origin requests which has the limitation that I can't separate the client from the backend which makes it less scalable then I wanted my software to be.

Please note that currently development is my hobby. I write these sentences primarily for myself so that I don't repeat mistakes in the next project. I'm happy if my writing helps some people and I'm also happy to get feedback and be corrected in case something is wrong. I work harder if I feel like an idiot.

If you are confident that you want to do the preflight dance... below I describe as far as I got and.... failed with OPTIONS request being blocked.

Adding Maven Dependency

First of all you must check which version of Jetty you are using. According to that you can search for the corresponding Maven jetty-servlets Version. Once determined you can add the dependency to your pom.xml.

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-servlets</artifactId>
            <version>8.1.9.v20130131</version>
        </dependency>

Configuring web.xml

As described above declarations such as allowedMethods and allowedHeaders won't work by setting a "*". You have to tell explicitly what you want to allow.

Additionally I noticed (after some weeks of frustration....) that the order you add filters in web.xml seems to matter. The CORS filter has to be added before the springSecurityFilterChain filter. Otherwise authorization doesn't seem to work. I'm not sure here but I guess that the headers first have to be allowed by CORS.

    <!-- CORS related filter -->
    <filter>
        <filter-name>CORS</filter-name>
        <filter-class>
          org.eclipse.jetty.servlets.CrossOriginFilter
        </filter-class>

        <init-param>
            <param-name>allowedOrigins</param-name>
            <param-value>*</param-value>
        </init-param>
       <init-param>
            <param-name>allowedMethods</param-name>
            <param-value>GET,POST,DELETE,PUT,HEAD</param-value>
        </init-param>
        <init-param>
            <param-name>allowedHeaders</param-name>
            <param-value>
               origin, content-type, accept, authorization, 
                  x-requested-with
            </param-value>
        </init-param>
        <init-param>
            <param-name>supportsCredentials</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>CORS</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- Spring Security Filters -->
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
    </filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

spring-security.xml

Finally create-session has to be set to stateless in the spring security configuration. Without enabling stateless authentication it can also lead to errors when executing a request from within Angularjs $http method.

<http use-expressions="true" create-session="stateless">
...
<http>

Test with Curl

You can test the headers with the following curl command below.

curl -v -X GET \
    -H "Origin: http://www.example.com" \
    -H "Authorization: Basic am9obkBqb2huLmNvbTpibGE=" \
    -H "Accept: application/json" \ 
    -H "Access-Control-Request-Headers: X-Requested-With" \  
    http://localhost:8080/api/user

The output should look similar to this.

* connected
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /api/user HTTP/1.1
> User-Agent: curl/7.27.0
> Host: localhost:8080
> Origin: http://www.example.com
> Authorization: Basic am9obkBqb2huLmNvbTpibGE=
> Accept: application/json
> Access-Control-Request-Headers: X-Requested-With
> 
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: http://www.example.com
< Access-Control-Allow-Credentials: true
< Access-Control-Expose-Headers: 
< Content-Type: application/json
< Transfer-Encoding: chunked
< Server: Jetty(8.1.9.v20130131)
< 
* Connection #0 to host localhost left intact
Here you should see the JSON DATA that is parsed back.
* Closing connection #0

Further resources

3 comments:

  1. Thanks you Chris! You are my hero!
    I got stuck in this for an entire afternoon, none of the google searches were helpful except yours!
    the key sentence that sets me free is: "As described above declarations such as allowedMethods and allowedHeaders won't work by setting a "*". You have to tell explicitly what you want to allow"

    ReplyDelete
  2. Thanks for posting this! I had spent the last few weeks trying to figure this one out, and moving the filter chain up was what I had been missing!!!

    ReplyDelete