Summary
Deprecation of third-party cookies breaks the existing business model for multi-brand publishers and advertisers without providing a sufficient alternative. Available approaches are either defunct due to more recent browser restrictions or cause an extremely degraded user experience which no publisher would want to impose on their readers or customers.
Introduction
MOW and Postindustria have recently joined forces to explore the publisher and user experience in the post-3rd party cookie world. The goal was to test the ability of a multi-brand publisher or advertiser to share arbitrary data (f.e. user ID or session info) between their websites.
Currently, third-party cookies are still supported by certain web browsers such as Chrome. This allows a website to store some information in the context of the other site. However, the upcoming deprecation of third-party cookies means that this approach will no longer work. To mitigate the consequences of this, Google has provided two frameworks: Storage Access API and Related Web Sites Sets. These frameworks allow data sharing via third-party cookies in some circumstances.
In this blog we describe the results of implementing 4 cross-site data-sharing approaches available to a typical multi-site publisher.
Setup
We replicated a typical multi-brand publisher infrastructure, including eight branded websites and one service domain responsible for sharing the data between the sites:
Service Domain:
Sites:
- https://philolaus.uk
- https://miltiades.uk
- https://meleager.uk
- https://epicurus.uk
- https://aesara.uk
- https://aegeus.uk
- https://actaeon.uk
- https://themistocles.uk (the set primary for RWS and not configured to share data)
The service domain is used to share data between the sites. It doesn’t host any UI, instead it exposes two endpoints:
/media-id
– returns the page with the javascript that works with the local storage and Storage Access API. This page is embedded into the other sites in theiframe
. The response also sends the SetCookie header with a randomly generated phrase./get-id
The subject of sharing is a 4-word randomly generated phrase. The words are obtained from the https://random-word-api.vercel.app/api?words=4 public API call. An example of the phrase: ageless-unclaimed-ecosphere-caption
.
The content of the branded sites came from https://themeforest.net/ under their free use noncommercial license.
Data sharing mechanisms
The four cross-site data sharing approaches implemented are:
- Third-party cookies – the shared data is sent via the HTTP headers.
- Local Storage – the shared data is written to and read from the local storage by the javascript code.
- Storage Access API (SAA) – the shared data is put into the unpartitioned cookie storage utilizing the SAA framework.
- Related Website Sets (RWS) – we prepared the sites’ configuration, and the pull request to register the current infrastructure in RWS. After the merge, the behavior of the SAA approach should be changed according to the RWS specification.
The following storage keys are used:
mow-media-identifier-3p
– the key for the cookie set via theSetCookie
header (no javascript client code needed).mow-media-identifier-localStorage
– the key for the value set into the local storage (by the javascript client code).mow-media-identifier-saa
– the key for the cookie set viadocument.cookie
once the access is allowed after using SAA (by the javascript client code).
A common integration approach was used to manage these data-sharing variants. The iframe
with the code served from the service domain was embedded into each site in the set. The service domain provides the code to share data.
<iframe id="media-id" src="https://soterias.uk/media-id"
sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox allow-modals">
</iframe>
The sandbox attribute grants the iframe
the following privileges:
- Request access to unpartitioned cookies via Storage Access API.
- Open the new tab in the browser with the page of service domain, also needed to complete the SAA flow.
The /media-id
endpoint implements the following actions:
- If the request contains the
Cookie
header with a value for themow-media-identifier-3p
, the response will contain the same value in theSetCookie
HTTP response header. - If there are no cookies for
mow-media-identifier-3p
, the arbitrary data will be generated on the server and returned in the SetCookie HTTP response header. - In the response, the page with the javascript code will be served that:
- Sets the data into local storage if empty.
- Runs the SAA flow to get access to the unpartitioned cookies and stores data in it.
- Sends data from cookies and local storage to the main site via
window.postMessage()
method call.
The javascript mimics an arbitrary identity provider logic – that is cross-site user ID sharing. No additional business logic or UI is implemented to not obscure the purposes of the experiment.
Third-party Cookie
The classical cross-site data sharing approach using HTTP SetCookie/Cookie headers in our setup works the following way:
Step 1: The browser loads makes a request to the /media-id
endpoint. If the cookie was already set previously for the service domain (mow-media-identifier-3p
), it would be sent in the Cookie
HTTP request header.
Step 2: The service domain checks the Cookie
header in the request, and if it is empty, generates an arbitrary string and sends it back in the SetCookie
HTTP response header.
Step 3: The browser stores the cookie received in the SetCookie
HTTP header attached to the service domain.
Step 4: The service script reads the stored cookie data and sends it to the main page using the post message API.
Step 5: The site displays the data.
If, after that, the user visits another site of the same publisher, the same steps will be performed. However, if 3rd party cookies are still supported, the browser will have the cookie stored, so the server will not generate a new phrase, and the data stored in the cookie will be shared with another publisher’s site.
Local Storage
The browser’s local storage API could be used as an alternative to 3rd party cookies. This API is widely used by identity providers and also allows (or at least allowed until recently) data sharing across sites. In our setup, it works the following way:
Step 1: The browser loads the iframe
content, making the request to the /media-id
endpoint.
Step 2: The service script checks the value in the local storage. If there is no value, it makes a request to the service domain (/get-id
) to generate arbitrary data. The service script puts the data into the local storage.
Step 3: The service script passes the data from the local storage to the main page using the post message API.
Step 4: The site displays the data
If, after that the user visits another site for the same publisher, the same steps will be performed. The expected behavior is that the browser already has the data in the local storage for the service domain, so the server won’t generate a new word. The same data stored in the local storage for the service site will be shared across the sites.
However, this approach already does not work in Chrome, starting version 115, Safari version 12 and Firefox version 85. From these versions, these browsers implement state partitioning. It means that the content of the local storage (and other stateful APIs) is tied to the parent domain in the third-party context. More info on this is available in the State Partitioning specification.
The content of the local storage inside the service domain iframe
when embedded into website A is not available inside the same iframe
when it is embedded in website B. Even though both iframe
s load their content from the same origin (service domain).
Storage Access API
The third alternative is Storage Access API (SAA). The service domain script can use this API to get access to the unpartitioned third-party cookies (that are not restricted to each first-party domain). However, this API has at least two strict requirements that complicate the solution and break the UX:
- Users must visit the site hosted on the service domain directly before the script requests access to the cookies.
- The request to access data should be made in the user interaction handler, f.e. during click.
SAA flow looks like this:
This approach allows data to be shared cross-site if the user allows this. However, it breaks the flow and UX tremendously.
Step 1: The browser loads the iframe
content, making the request to the /media-id
endpoint.
Step 2: The service script checks access to the cookies (using the hasStorageAccess()
method). If access is denied:
Step 2.1: The script renders the button “Request Storage Access” on the publisher’s site
Step 2.2: Once the user clicks the button, the script calls the requestStorageAccess method. If access is not allowed:
Step 2.2.1: The script displays the button “Visit Service Site” which user must click on.
Step 2.2.2: The script opens the /terms-of-service
page of the service to satisfy the SAA requirement.
Step 2.2.3: The user returns to the site and to the step 2.1 of the current flow.
Step 2.3: The browser shows the prompt to ask the user to allow sharing data between the current site and the service site.
Step 2.4: If the user clicks the “Allow” button on the prompt, the browser provides access to the unpartitioned cookies
Step 3: The script checks if document cookies contain a value. If not:
Step 3.1: The script makes a request to the service domain (/get-id
) to generate a random phrase.
Step 3.2: The Service script sets the received data to the document.cookie
property.
Step 4: The service script passes the data from the document.cookie
to the main page using the window.postMessage()
method.
Step 5: The site displays the data
For example, if the user has not visited the service domain before, the requestStorageAccess()
will throw an exception with the following error message:
requestStorageAccess: Request denied because the embedded site has never been interacted with as a top-level context.
According to SAA specification: If the promise rejects (i.e. permission was not granted), then the user gesture has been consumed, so the script can’t do anything that requires a gesture. This is intentional protection against abuse — it prevents scripts from calling requestStorageAccess()
in a loop until the user accepts the prompt.
To eventually be navigated to the service site, the user has to click the button one more time. It will be shown later in the demo section of this report.
Related Websites Set
The RWS is a part of the Google Privacy Sandbox framework dedicated to providing automatic (without user prompt) access to unpartitioned cookies in the third-party context for the sites and services that are declared to be part of the multisite ecosystem.
Relying on the RWS documentation, we expected to see that the SAA approach would work for our multi-brand publisher setup without prompting the user to provide access.
In addition, we wanted to verify how exactly the browser will behave when the number of associated sites is more than 5. According to the RWS documentation, the browser should still show the prompt in this case, which is unacceptable behavior for a multi-brand publisher.
However, we haven’t had a chance to verify this because Google rejected our PR starting the following “reasons”:
Not a contribution statement in the PR
Suspicious sites
The conversation is preserved in the PR:
https://github.com/GoogleChrome/related-website-sets/pull/148
The rejection reasons are nonsensical for:
- We put the note “NOT A CONTRIBUTION” because of the Google’s CLA that we signed previously.
- The alleged copyright issues are invalid (see the Setup section of this post which explains the license used for the templates) and even if they were – they are not relevant here since copyright is completely orthogonal to Related Websites Set permissions.
The end result is that for non-technical reasons RWS is not usable by any business that does not wish Google to make unjustified judgements concerning its operations or is not willing to accept a one sided usage agreement.
Demo
The reader is invited to validate the scenarios described above using the sites listed in the beginning of this post. The following screenshots demonstrate the main results and UI/UX of the solution. The reader can also watch the full video of the experiment.
The legend for the following demo screenshots:
- The websites render a panel on the top and demonstrate the values passed from the service
iframe
. - There are three sections in the panel:
- Cookie: displays the phrase stored in cookies received in the SetCookie header.
- Local Storage: displays the phrase stored in the local storage of the
iframe
. - Cookie (SAA): displays the phrase stored in cookies that set via the service script code after the access to the unpartitioned cookies in granted.
- The service script may render the buttons to prompt user perform interaction needed to:
- Request Storage Access – run the SAA flow if third party cookies are deprecated and SAA is available
- Visit Service Site – open the new tab in the browser with the page shipped from the service domain.
The current Behavior
The following screenshots demonstrate the behavior of the most recent Chrome and Safari browsers. Looking at the header sections of the sites, you can see that:
- Chrome:
- Cookie: displays the same phrase across two websites because the third-party cookies are still available.
- Local Storage: displays different phrases on sites because the local storage is partitioned.
- Cookie (SAA): displays the same phrase across two sites.
- Safari:
- Cookie: is empty because the browser doesn’t allow it to store data in third-party cookies.
- Local Storage: displays different phrases on websites because the local storage is partitioned.
- Cookie (SAA): is empty because the browser doesn’t allow it to store data in third-party cookies.
- The Request Storage Access button is present because Safari implements the Storage Access API.
Chrome: Version 120.0.6099.129 (Official Build) (arm64) |
https://actaeon.uk |
https://epicurus.uk |
Safari: Version 17.1 (19616.2.9.11.7) |
https://actaeon.uk |
https://epicurus.uk |
Blocked third-party Cookies and SAA
The following screenshots demonstrate the use case of getting access to the unpartitioned cookies via the Storage Access API in Chrome. The Safari behavior is not demonstrated since it will be almost the same. Inspecting the screenshots, you will see that:
- Chrome with disabled third-party cookies:
- Cookie: is empty because the browser doesn’t allow it to store data in third-party cookies.
- Local Storage: displays different phrases on websites because the local storage is partitioned.
- Cookie (SAA): is empty because the browser doesn’t allow it to store data in third-party cookies.
- The Request Storage Access button is present because Chrome implements the Storage Access API.
- Chrome with granted access to the unpartitioned cookies:
- Cookie: is empty because the browser tries to request access to the unpartitioned storage using SAA before writing data. Therefore, storing cookies from the header is still not allowed.
- Local Storage: displays different phrases on websites because the local storage is partitioned.
- Cookie (SAA): displays the same phrase across two sites because the browser granted access.
The following chart demonstrates the steps that users must pass to allow the browser to share data between websites of multi-brand publishers that use the SAA.
Prerequisites: Phase out third-party cookies |
chrome://flags |
Step 1: Request Storage Access (website A) While the page is loading, the iframe validates the access to unpartitioned cookies, and if it is denied, the script shows the button that the user must click in order to request storage access. |
https://actaeon.uk |
The storage access is not allowed because the user has never visited the service domain yet. The iframe changes the button that now opens the service site. |
https://epicurus.uk |
Step 2: Visit the service domain. |
https://soterias.uk |
Step 3: Request the storage access one more time (website A) The iframe shows the button for requesting storage access one more time because the user interaction is required to call SAA’s requestStorageAccess() function. Once the button is clicked, the browser shows the prompt. If the user allows the data sharing, access to the cookies will be provided, and the iframe can put data into it and share it with the parent site. |
https://actaeon.uk |
Step 4: Share the data with the parent site (website A) |
https://actaeon.uk |
Step 5: Visit another site (website B) The access to unpartitioned cookies is denied for the service iframe on this website. The script shows the button that the user must click in order to request storage access. |
https://epicurus.uk |
Step 6: Request the storage access one more time The same procedure of requesting access via user interaction is required for each site of the multi-brand publisher. But there is no need to visit the service site anymore. |
https://epicurus.uk |
Step 7: Verify the shared data (step 4) |
https://epicurus.uk |
Therefore, sharing data for multi-site publishers using post-cookie approaches is useless due to degraded UX and overcomplication.
Video
To see all the described steps in action – watch the following video:Desktop (full experiment):
[UPD 8th of January, 2024] RWS Enabled
The current section describes the browser behavior with enabled RWS.
At the moment of writing the blog post, the MOW’s PR wasn’t merged yet, so we were not able to test and demo the full-featured behavior. We will do it later in the additional section, once the MOW’s set is merged into the canonical list.
For now, we describe the behavior with a locally registered set.
Test Locally
In order to test RWS locally, we need to launch Chrome in developer mode with the following arguments:
$ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --use-related-website-set="{\"primary\":\"https:\/\/themistocles.uk\",\"associatedSites\":[\"https:\/\/philolaus.uk\",\"https:\/\/miltiades.uk\",\"https:\/\/meleager.uk\",\"https:\/\/epicurus.uk\",\"https:\/\/aesara.uk\",\"https:\/\/aegeus.uk\",\"https:\/\/actaeon.uk\"],\"serviceSites\":[\"https:\/\/soterias.uk\"]}"\
https://themistocles.uk/
After that we can check the provided set is added:
Now we can clearly see that Chrome adds not all items from the associatedSites
list but only 5 of them.
{
"AssociatedSites": [ "https://aesara.uk", "https://epicurus.uk", "https://meleager.uk", "https://miltiades.uk", "https://philolaus.uk" ],
"PrimarySites": [ "https://themistocles.uk" ],
"ServiceSites": [ "https://soterias.uk" ]
}
Now, we can conduct the experiment one more time to see how the browser will behave with enabled RWS. The following table illustrates the difference in the behavior of the websites presented in the browser’s list and websites that were not included:
Website | Present in the browser’s set | The user action to request access to unpartitioned cookies | Visit the service domain before requesting the access to unpartitioned cookies | User prompt |
https://actaeon.uk | No | Required | Required | Shown |
https://epicurus.uk | Yes | Required | Not Required | Is not shown |
The following video demonstrates the behavior in dynamic:
[UPD 15th of January, 2024] Resubmit the PR to the RWS canonical list
We addressed some issues in the initial PR, like “text is copied and pasted from another site” and assets under copyright. But still put the label “NOT A CONTRIBUTION” according to the legal suggestions dictated by Google’s CLA. And we are not willing to share our intellectual property with Google.
https://github.com/GoogleChrome/related-website-sets/pull/202
The result of the review was the same as in the previous time: