privacycg / CHIPS

A proposal for a cookie attribute to partition cross-site cookies by top-level site
Other
133 stars 31 forks source link

Safely migrating existing web app cookies to Partitioned #78

Closed GREsau closed 10 months ago

GREsau commented 1 year ago

TL;DR: Is there a recommended process for migrating a web app to issue cookies as Partitioned which does not break ongoing cookies/sessions?

We currently have a big, old, complex, monolithic web app which uses cookies mostly for authentication. Our app is sometimes used in a cross-origin iframe e.g. embedded in a CRM, and sometimes used in a top-level context (and sometimes both, in the same browser session).

To avoid being broken by third-party cookie blocking, we are intending to add the Partitioned attribute to all cookies issued by our app. Being unable to share cookies between the iframe and top-level context is a bit of a problem, but we're mostly working-around it with some (unfortunately slightly hacky) popups and window.opener shenanigans.

Our biggest problem is finding a way to safely roll-out the change to make cookies Partitioned in a backward-compatible manner (and forward-compatible, in case we need to temporarily roll-back the change), which we need to do before browsers actually start blocking third-party cookies. Some of our enterprise customers have quite slow-moving browser update policies, which means our solution should ideally still work for browsers that don't understand/respect the Partitioned attribute.

Naively making all new Set-Cookie headers have the Partitioned attribute would cause problems for users who have ongoing sessions at the point we apply the change, e.g.

  1. A user has an unpartitioned cookie auth=some.session.data
  2. We update our app to include Partitioned in all Set-Cookie headers
  3. The user tries to log out, causing the server to send a response header like Set-Cookie: auth=; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Partitioned
  4. Because the existing cookie in the browser is unpartitioned, the browser does not delete the cookie, so the user remains logged in despite their best efforts

We can work-around this specific case by, when issuing Set-Cookie header with an Expires value in the past (i.e. when deleting a cookie), duplicate the header - once with Partitioned, and once without. This ensures that we delete the cookie no matter whether it was issued as Partitioned or not - although it does violate the recommendations of RFC 6265:

Servers SHOULD NOT include more than one Set-Cookie header field in the same response with the same cookie-name.

However, we still have a problem when trying to update an existing cookie without deleting it, e.g.

  1. A user has an unpartitioned cookie auth=some.session.data
  2. We update our app to include Partitioned in all Set-Cookie headers
  3. The user's session data changes (e.g. when it is renewed) causing the server to send a response header like Set-Cookie: auth=renewed.session.data; Partitioned
  4. Because the existing cookie in the browser is unpartitioned, the browser does not replace the existing cookie - it instead adds a new cookie with the same name
  5. Subsequent requests from the browser include both cookie values in the request header, e.g. Cookie: auth=some.session.data; auth=renewed.session.data

In this case, we would generally want the server to always use the most recently-set cookie value. But with the current browser behaviour, we would need to rely on this always being the last value for the cookie name - this is also against RFC 6265:

Although cookies are serialized linearly in the Cookie header, servers SHOULD NOT rely upon the serialization order. In particular, if the Cookie header contains two cookies with the same name (e.g., that were set with different Path or Domain attributes), servers SHOULD NOT rely upon the order in which these cookies appear in the header.

krgovind commented 11 months ago

Thanks for the question, @GREsau!

First, as it pertains to finding a more elegant solution for your use-case, I would recommend looking into the FedCM API, since that is the preferred solution to keep login working post-third-party cookie phaseout.

Secondly, note that for Chrome, we are planning to deploy some short-term heuristics to keep login use cases such as yours working; so if your user flow triggers the heuristics, you may find that your iframe continues to retain access to unpartitioned third-party cookies.

Now, getting to your question - given the complications that you pointed out with using identical cookie names with different attributes, I think it may be best to maintain two duplicate/synced cookies with different names for the medium-term.

This means, you should keep your current auth cookie without the Partitioned attribute; but temporarily introduce a new cookie called auth-partitioned that is synced to contain the same value as the auth cookie, but additionally has the Partitioned attribute specified.

Do you see any issues with this scheme?

GREsau commented 10 months ago

Hi @krgovind, thanks for the info - and sorry for the slow response, I was off of work for most of december

We looked into FedCM but it wasn't really viable for us in the short/medium-term for a few reasons. Off the top of my head, these include:

I don't think the exemption heuristics would work for us without tweaking our auth flow, since the first cookie is set before any user interaction (before the login page is even displayed). Even if it did work, this would of course just be kicking the can down the road - assuming these heuristics are just temporary, we would still need to find a longer-term fix.

Introducing new partitioned cookies with an extra -partitioned (or other arbitrary string) added to the name would probably work, but there are still significant challenges with that solution. Mainly, we would need to update all usages of all cookies within our app - for the trivial example where there's a single "auth" cookie, this would be fine, but of course in practice it's not so simple. As I said above, we're unfortunately dealing with "a big, old, complex, monolithic web app". This has many usages of cookies in both first-party code and third-party libraries, some of which are read by JavaScript on the front-end or layer-7 load balancers - so all of these would need updating. The cookies we're setting are also fairly large, so we've had to take steps in the past to compress them to avoid hitting browser/proxy cookie/header limits - duplicating each cookie would cause us to hit those limits again (we actually tried a solution similar to this earlier, and ran into this exact problem in our test environment!).

For now, the solution we're going ahead with is basically to:

Our full solution is a little more complicated with some extra steps in order to facilitate a backward-/forward-compatible "expand-contract" rollout, but that's the gist of it.

This means that we don't need to update any code that reads cookies, since they'll have the same name whether or not they're partitioned. It does mean we need to run some extra logic when setting cookies, but fortunately we're able to do this for all cookies with a custom module on our webserver, so we don't need to hunt down each individual place in code where we set a cookie.

This does of course mean we will be violating the "SHOULD NOT"s of RFC 6265 which isn't great. But practically, I think the risk of breaking non-compliant browsers is less than the cost of having to find alternative solutions that we can implement in a timely manner.

krgovind commented 10 months ago

@GREsau Thanks for the feedback regarding FedCM. I'll pass this on to others on my team who are thinking about login use-cases.

Glad to hear that you've figured out a suboptimal scheme, but one that works for you.

samdutton commented 2 months ago

Is there a recommended process for migrating a web app to issue cookies as Partitioned which does not break ongoing cookies/sessions?

Very belated reply: Transition from unpartitioned to partitioned cookies provides a basic overview of this process.