CORS preflight issues in Firefox and Chrome

We are running into an issue making a Cross Origin call to the Bonita API. The issue we are having is similar to the problem described in the linked post but the solution to that question is not fixing the problem in our current environment:

http://community.bonitasoft.com/answers/cors-ajax-bonita-rest-ap-and-tomcat7

What I am seeing is when I make a call that causes a preflight (OPTIONS) request it is failing with a 401 Unauthorized response in Firefox. It is working in Chrome; however, I believe that is because of an issue in Chrome and Firefox is actually doing what it is supposed to do which causes the failure (this should make more sense as you read on).

Here is the request in Chrome. Notice the cookie:

OPTIONS /bonita/API/bpm/humanTask?p=0&c=100&f=state=ready HTTP/1.1
Host: X.X.X.X:8080
Connection: keep-alive
Access-Control-Request-Method: GET
Origin: http://myworkstation.company.com:8082
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Access-Control-Request-Headers: accept, content-type
Accept: /
Referer: http://myworkstation.company.com:8082/index.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: JSESSIONID=EF06ABFF189A89F8D95791F44BD7B8C3; BOS_Locale=en

And here is the response in Chrome:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Access-Control-Allow-Origin: http://myworkstation.company.com:8082
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, HEAD, OPTIONS, POST, DELETE, PUT
Access-Control-Allow-Headers: Content-Type, Accept
Content-Length: 0
Date: Wed, 02 Jul 2014 12:45:24 GMT

Here is the request in Firefox. No cookie is passed:
OPTIONS /bonita/API/bpm/humanTask?p=0&c=100&f=state=ready HTTP/1.1
Host: X.X.X.X:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:30.0) Gecko/20100101 Firefox/30.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://myworkstation.company.com:8082
Access-Control-Request-Method: GET
Access-Control-Request-Headers: content-type
Connection: keep-alive

Here is the response in Firefox:
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
Set-Cookie: JSESSIONID=DC1E9B22973D2EF3EBB587E11BCD03BC; Path=/bonita/; HttpOnly
Content-Type: text/html;charset=utf-8
Content-Length: 951
Date: Wed, 02 Jul 2014 12:40:49 GMT

I believe the reason this is working in Chrome is because Chrome is passing the Cookie; however, according to w3.org, a CORS preflight request shouldn’t expect a cookie; which makes sense. A preflight call is a call to determine if an action is allowed. It should not require credentials to determine if I can do something, it should only require credentials to actually do it.

This is similar to an issue seen in the Twitter API which may explain the problem better than I am: https://code.google.com/p/twitter-api/issues/detail?id=2273

Finally, I realize this is a GET request and the only reason that it is causing a preflight is because I am setting the Content-Type header to application/json. I could fix this particular instance of the issue by removing the Content-Type on the GET; however, I am still making post calls at some point and if I do not specify the Content-Type as application/json on my posts (i.e. the API/bpm/case call in order to start a case, see example below). Setting application/json as the Content-Type will always cause a preflight request so I do not see a way to avoid the issue.

var startCase = function startCase(processId){
return $.ajax({
xhrFields: {withCredentials: true},
contentType: “application/json”, //Required or you get a 500 error
url: bonitaUrl + “API/bpm/case/”,
type: ‘POST’,
data: ‘{“processDefinitionId”:"’ + processId + ‘"}’
});
}

This can be solved (from what I can tell) if Bonita changes the API so that a preflight (OPTIONS) request does not require authorization.

This is a real dealbreaker: the set-cookie is httpOnly, how are we supposed to read it to set it subsequent ajax calls?

Some additional information may be helpful.

I am running the ajax code locally on my workstation.

I am connecting to a subscriber standalone Bonita instance. The Bonita version is 6.2.3. The tomcat version is 7.0.33.

I am using a third party libraries to enable the CORS filter in tomcat. The website to download the libraries is http://software.dzhuvinov.com/cors-filter-installation.html

Inside of /usr/share/tomcat/conf/web.xml I have the CORS filter setup as follows:


CORS
com.thetransactioncompany.cors.CORSFilter

    <init-param>
        <param-name>cors.allowOrigin</param-name>
        <param-value>http://myworkstation.company.com:8082, http://otherworkstation.company.com:8082</param-value>
    </init-param>

    <init-param>
        <param-name>cors.supportedMethods</param-name>
        <param-value>GET, HEAD, POST, PUT, DELETE, OPTIONS</param-value>
    </init-param>

    <init-param>
        <param-name>cors.supportedHeaders</param-name>
        <param-value>Accept, accept, Content-Type, content-type</param-value>
    </init-param>

I have commented out this line in /usr/share/tomcat/conf/server.xml

Update: This no longer works in the latest version of Chrome either. I assume that the bug in Chrome that was allowing this to work has been patched.

You don’t need to set the cookie directly. You chain your calls. Here are some example functions from a proof of concept site that I wrote:

var bonitaUrl = "http://localhost:8080/bonita/";

var loginBonita = function loginBonita(){
return $.ajax({
xhrFields: {withCredentials: true},
contentType: “application/x-www-form-urlencoded”,
url: bonitaUrl + “loginservice”,
type: ‘POST’,
data: ‘username=walter.bates&password=bpm&redirect=false’
});
};

var logoutBonita = function logoutBonita(){
    return $.ajax({
        xhrFields: {withCredentials: true},
        url: bonitaUrl + "logoutservice?redirect=false",
        type: 'GET',
    });
};

var getHumanTasks = function humanTasks(){
    return $.ajax({
        xhrFields: {withCredentials: true},
        url: bonitaUrl + "API/bpm/humanTask?p=0&c=100&f=state=ready",
        type: 'GET'
    });
};

var getProcesses = function getProcesses(){
    return $.ajax({
        xhrFields: {withCredentials: true},
        url: bonitaUrl + "API/bpm/process?p=0&c=100",
        type: 'GET'
    });
};

var getUsers = function getUsers(){
    return $.ajax({
        xhrFields: {withCredentials: true},
        url: bonitaUrl + "API/identity/user?p=0&c=100",
        type: 'GET'
    });
};

To get the human tasks (and a little more information) I would do something like this:

var getRunningTasks = function() { // We need to get a list of tasks from Bonita. The tasks list returns the process id and a user id (if the task is assigned). // We want to display the process name and the assigned user name so we need to make additional calls to get that data. // Since this is asyncronous code we need to gather the data up and then put it all together. $.when(loginBonita()).done(function(l){ // When all calls are done put things together... $.when(getHumanTasks(), getProcesses(), getUsers()).done(function(t, p, u){ //When the human task call is done. logoutBonita(); // Logout. No $.when call here because we don't care how log it takes to log out. var processName = ""; // Variable to store the current process name. var assigneeUserName = "" // Variable to hold the current assignee user name. //Loop through all the processes from Bonita. for(var i = 0; i < t[0].length; i++) { // Simple loop to get the current process name. We aren't dealing with a lot of data // so we don't need anything fancy. processName = "Unknown"; // Set to a default value in case is isn't found (which should never happen here). for (var j = 0; j < p[0].length; j++){ if (p[0][j]["id"] == t[0][i]["processId"]){ processName = p[0][j]["name"] break; // We found what we needed. Stop looking... } } // Simple loop to get the current assignee user name. We aren't dealing with a lot of data // so we don't need anything fancy. assigneeUserName = "Unassigned"; // Set to a default value in case it isn't found (which might happen here). for (var j = 0; j < u[0].length; j++){ if (u[0][j]["id"] == t[0][i]["assigned_id"]){ assigneeUserName = u[0][j]["userName"]; } } // Add the data to the processes table. $('#processes tbody').append('<a href="javascript:void(0);" onclick="javascript:BonitaPOC.switchToWorkOrder(' + t[0][i]["caseId"] +');"' + t[0][i]["caseId"] + '' + processName + ''+ t[0][i]["displayName"] + '' + assigneeUserName + ''); } }) }); };

The problem I am having is that the Bonita API seems to expect the credentials to be passed on a pre-flight (OPTIONS) call most browsers don’t pass (and shouldn’t from what I can tell). I only see this when running on a remote server (or any other configuration that forces a CORS call). I do not know if this is a problem with the latest version of Bonita. I ended up creating a facade class using Ruby and Sinatra for the pieces of the Bonita REST API that I use so I wouldn’t need to worry about it anymore.