Due to decisions that are completely outside of my control, I am in the following situation:
I have a product listing on catalog.org
Clicking the "Add to Cart" button on a product makes an AJAX JSONP request to secure.com/product/add/[productKey], which saves the cart record to the database, sets a cookie with the cart ID, and returns a true response (or false if it failed)
Back on catalog.org, if the response is true, another AJAX JSONP request is made to secure.com/cart/info, which reads the cart ID cookie, fetches the record, and returns the number of items in the cart
Back on catalog.org once again, the response is read and an element on the page is updated showing the number of items in the cart (if any)
At this point, clicking the "Go to Cart" button on catalog.org displays the cart summary on secure.com
This works beautifully in Firefox 17, Chrome 32 and IE 11. It also works in IE8 - IE10 on our development and test environments, where catalog.org is catalog.development.com and catalog.test.com and secure.com is secure.development.com and secure.test.com respectively.
However, after we deployed to production, this stopped working in IE8 - IE10. After adding a product to the cart, the number of items in the cart is updated successfully on catalog.org. Then, after clicking the "Go to Cart" button on catalog.org, the cart summary on secure.com shows nothing because it can't read the cookie. Going to Cache > "View cookie information" in IE develeoper tools shows no cart ID cookie. It should be there, just like it is there in other browsers and in our development and test environments.
I believe what's happening is IE is blocking third party cookies. We have added a P3P compact policy header to all requests on secure.com, but the cookie is still not being set. The header we are setting is:
P3P: CP="CAO PSA OUR"
Why doesn't adding the compact policy header fix this in IE8 - IE10? How can I fix this to work in all versions of IE?
Solution
There are several good ideas posted below. I accepted @sdecima's because it sounded the most promising. We ended up combining some of these ideas but managed to avoid XDomainRequest:
- Clicking the "Add to Cart" button on a product makes an AJAX JSONP request to secure.com/product/add/[productKey], which saves the cart record to the database, sets a cookie with the cart ID, and returns a true response (or false if it failed)
We changed the action at secure.com/product/add to return a JSON object with a boolean indicating success or failure and the cart ID.
- Back on catalog.org, if the response is true, another AJAX JSONP request is made to secure.com/cart/info, which reads the cart ID cookie, fetches the record, and returns the number of items in the cart
We changed the callback function to check for both properties in the response object. If success is true and the cart ID is present, we create a hidden iframe on the page. The src
attribute of the iframe is set to a new endpoint we added to secure.com. This action accepts a cart ID parameter and saves the cart ID cookie. We no longer need to save the cookie in the secure.com/product/add action.
Next, we changed the action at secure.com/cart/info to accept a cart ID parameter. This action will use the cart ID parameter if present to fetch the cart information, otherwise it will still attempt to read the cookie. This extra check would be unnecessary if we could guarantee that the iframe had finished loading and the cookie had been saved on secure.com, but we have no way of knowing when the iframe has finished loading on catalog.org due to browser security restrictions.
Finally, the P3P header CP="CAO PSA OUR"
is still required for this to work in IE7 - IE10. (Yes, this works in IE7 now too :)
We now have a solution (albeit an incredibly complex one) for saving and accessing cross domain cookies that works in all major browser, at least as far back as we can reliably test.
We will probably refactor this some more. For one thing, the second AJAX JSONP request to secure.com/cart/info is redundant at this point since we can return all the information we need in the original request to secure.com/product/add action (a side benefit of changing that action to return a JSON object - plus we can return an error message indicating exactly why it failed if there was an error).
Cookies will NOT go through a cross-origin request on IE 8 and 9. It should work on IE 10 and 11 though.
On IE8/9 XMLHttpRequest partially supports CORS, and cross-origin requests are made with the help of the XDomainRequest object which does NOT send cookies with each request.
You can read more about this on the following official MSDN Blog post:
http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx
Particularly this part:
5 . No authentication or cookies will be sent with the request
In order to prevent misuse of the user’s ambient authority (e.g. cookies, HTTP credentials, client certificates, etc), the request will be stripped of cookies and credentials and will ignore any authentication challenges or Set-Cookie directives in the HTTP response. XDomainRequests will not be sent on previously-authenticated connections, because some Windows authentication protocols (e.g. NTLM/Kerberos) are per-connection-based rather than per-request-based.
Starting with IE10, full CORS support was added to XMLHTTPRequest and it should work fine with a correct Access-Control-Allow-Origin header property on the response from the server (that wishes to set the cookie on the browser).
More about this here:
http://blogs.msdn.com/b/ie/archive/2012/02/09/cors-for-xhr-in-ie10.aspx
And here:
http://www.html5rocks.com/en/tutorials/cors/
The only way to go around this on IE8/9 is, quoting the same MSDN post as above:
Sites that wish to perform authentication of the user for cross-origin requests can use explicit methods (e.g. tokens in the POST body or URL) to pass this authentication information without risking the user’s ambient authority.
Bottom line: third party cookies are commonly blocked by privacy/advertisement blocking extensions and should be considered unreliable. You'll be shooting yourself in the foot leaving it in production.
The syntax suggests that the endpoint has ambitions to one day become RESTful. The only problem with that is using cookies, which throws the whole "stateless" concept out of the window! Ideally, changes should be made to the API. If you are not integrating with a third party (i.e. "secure.com" is operated by your company) this is absolutely the correct way to deal with the issue.
Move the cartId
out of the secure.com cookie into its querystring:
secure.com/product/add/9876?cartId=1234 //should be a POST
Where to get a valid cartId
value? We can persist it in some secure-com-cart-id
cookie set for catalog domain, which will avoid any cross-domain issues. Check that value and, if present, append to every secure.com request as above:
$.post('secure.com/product/add/9876', { //needs jQuery.cookie
cartId: $.cookie('secure-com-cart-id')
});
If you don't have a valid cartId
, treat it as a new user and make the request without the parameter. Your API should then assign a new id and return it in the response. The "local" secure-com-cart-id
cookie can then be updated. Rinse and repeat.
Voila, you've just persisted an active user cart without polluting API calls with cookies. Go yell at your architect. If you can't do that (changing API syntax or yelling), you'll have to set up a tunnel to secure.com endpoint so that there will be no cross-domain request - basically something sitting at catalog.org/secure-com-endpoint which will channel the requests to secure.com verbatim. It's a workaround specifically to avoid making changes to the API, just don't do it with code and have proper Apache/IIS/F5 rules set up to handle it instead. A quick search comes up with several explanations, this one looks pretty good to me.
P.S.: this is a classic XY problem in my opinion. The solution isn't necessarily about persisting 3rd party cookies but about passing necessary parameters to a 3rd party while persisting the data somewhere.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With