IE8 - IE10 cross domain JSONP cookie headache
Asked Answered
M

5

5

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).

Maladminister answered 18/12, 2013 at 19:49 Comment(2)
How about using html's LocalStorage ?Reynaud
LocalStorage has per-domain restrictions as well I believe.Maladminister
B
7

In short

Cookies will NOT go through a cross-origin request on IE 8 and 9. It should work on IE 10 and 11 though.


IE 8 and 9

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.

IE 10+

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/

Workarounds on IE 8 and 9

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.

Burglary answered 10/1, 2014 at 3:28 Comment(3)
The only problem with this is lack of IE7 support. (Did I mention we have to support that POS too?) However, this sounds like a promising solution for IE8 - IE10. When it fails in IE7, we can probably fall back to a less elegant method.Maladminister
Cookies will not go cross-domain on IE7 nor on IE 8 or 9. You will need to find an alternate solution on those browsers. The workaround to use tokens/query-params on the URL or POST body is just a proposed solution; that BTW will work everywhere.Burglary
Accepted because I have to accept something. This answer is concise and offers several ideas which may work.Maladminister
M
3

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.

Matri answered 7/1, 2014 at 22:58 Comment(4)
Well, it works on our test environment because it isn't a cross-domain request in that context as both sites end in test.com. On our dev environment, both sites end in development.com.Maladminister
I can't count on always having the cart ID available when the user goes to secure.com. All our sites link to the cart in some fashion, plus the user can go there directly. The tunneling thing sounds like a major API change (basically do all the work of setting up the cart on catalog.org without any AJAX requests to secure.com), which is probably what we'll have to do.Maladminister
@davidkennedy85: cross-domain vs cross-subdomain is irrelevant for this solution, we didn't even have to use jsonp! I'll expand the answer a little bit on "not always having cart ID" though.Matri
We can use this solution for the site in question (catalog.org) but it wouldn't work for our other sites.Maladminister
A
0

Although a correct solution would be a change of architecture, if you're looking for a quick, temporary solution:

JSONP files are actually just javascript. You could add a line of code to set cookies to the front of your JSONP.

eg instead of:

callback({"exampleKey": "exampleValue"});

Your JSONP could look like:

document.cookie="cartID=1234";
callback({"exampleKey": "exampleValue"});
Apparition answered 12/1, 2014 at 9:11 Comment(3)
The problem isn't reading the cookie from catalog.org but reading it when the user goes back to secure.com.Maladminister
Open an iframe to secure.com, with the details in the url eg. http://secure.com/cookieSetter?exampleKey=exampleValue and have a javascript that reads it and sets the cookie.Apparition
We ended up having to use an iframe. I'll add the rest of the details to the question.Maladminister
B
0

If you control the DNS records, create a new entry so both servers are in the same domain.

Bighorn answered 13/1, 2014 at 5:45 Comment(4)
This is a public facing site.Maladminister
Lots of public facing sites have multiple dns entries. Are you saying that this is a public facing site which you do not have control over the dns records?Bighorn
I just mentioned this as it is an easy solution, if you can get your hands on the dns records....Bighorn
This is not an option as the domain is a business policy.Maladminister
B
0

Is there 1 database serving catalog.org and secure.com or can they communicate?

If so then, you got it.

When catalog.org servers a cookie, save it in the db. When secure.com servers a cookie, save it in the db. Then you can determine who's cart belongs to which user.

This is a fun problem to consider......Update 2:

When a user goes to catalog.org:

  • check if he has a cat_org cookie, if not, then:

    • in catalog.org:

      • create a key value pair and save in the db {cat_cookie_id, unique_number}
      • set cat_cookie_id in browser
      • instruct the browser to ajax visit secure.com/register/unique_number
    • in secure.com

      • read unique_number from url
      • create a secure_cookie id
      • save in the db {cat_cookie_id, unique_number, secure_cookie_id}
      • delete unique_number, as this is a one-time use key

Now the db can map cat_cookie_id to secure_cookie_id and vice versa.

Bighorn answered 14/1, 2014 at 17:15 Comment(2)
It has to work without authentication (think how Amazon works). I considered persisting the IP address if the user isn't signed in but, alas, the server is behind a load balancer and every request is from that IP. Furthermore, we have many clients that share IP addresses (think corporations).Maladminister
So you can't give a cookie to each catalog.org user? They would not need to login or anything, just give them a unique cookie if they do not have one already. I am sure Amazon.com does this. IP address are unreliable--good thinking out of the box though.Bighorn

© 2022 - 2024 — McMap. All rights reserved.