Back in 2019, I explained how browsers’ cookie controls and privacy features present challenges for common longstanding patterns for authentication flows. Such flows often rely upon an Identity Provider (IP) having access to its own cookies both on top-level pages served by the IP and when the IP receives a HTTP request from an XmlHttpRequest
/fetch
or frame embedded in a Relying Party (RP)‘s website:
These auth flows will fail if the IP’s cookie is not accessible for any reason:
While Cookie Partitioning is opt-in today, in late 2024, Chromium plans to start blocking all non-partitioned cookies in a 3rd Party context, meaning that authentication flows based on this pattern will no longer work. The IP’s top-level page will set the cookie, but subframes loaded from that IP in the RP’s page will use a cookie jar from a different partition and not “see” the cookie from the IP top-level page’s partition.
What’s a Web Developer to do?
The simplistic approach would be to have the authentication flow happen within the subframe that needs it. That is, the subframe to the IP within the RP asks the user to log in, and then the auth cookie is available within the partition and can be used freely.
Unfortunately, there are major downsides to this approach: every single relying party will have to do the same thing (no “single-sign on”), and worse, the user will have to be accustomed to entering their IP credentials within a page that visually has no relationship to the IP (because only the RP’s URL is shown in the browser UI). I would not recommend anyone build a design based on the user entering, for example, their Google.com
password within RandomApp.com
.
If we take that approach off the table, we need to think of another way to get an authentication token from the IP to the RP, which factors down to the question of “How can we pass a short string of data between two cross-origin contexts?” And this, fortunately, is a task which the web platform is well-equipped to solve.
One approach is to simply pass the token as a URL parameter. For example, the RP.com
website’s login button does something like:
window.open('https://IP.com/doAuth?returnURL=https://RP.com/AuthSuccess.aspx?token=$1', 'blank');
In this approach, the Identity Provider conducts its login flow, then navigates its tab back to the caller-provided “return URL”, passing the authentication token back as a URL parameter. The Relying Party’s AuthSuccess.aspx
handler collects the token from the URL and does whatever it wants with it (setting it as a cookie in a first-party context, stores it in HTML5 sessionStorage
, etc). When the token is needed to call an service requiring authentication, the Relying Party takes the token it stored and adds it to the call (inside an Auth header, as field in a POST body, etc).
One risk with this pattern is that, from the web browser’s perspective, it is nearly indistinguishable from bounce tracking, whereby trackers may try to circumvent the browser’s privacy controls and continue to track a user even when 3rd party cookies are disabled. While it’s not clear that browsers will ever fully or effectively block bounce trackers, it’s certainly an area of active interest for them, so making our auth scheme look less like a bounce tracker seems useful.
So, my current recommendation is that developers communicate their tokens using the HTML5 postMessage API. In this approach, the RP opens the IP and then waits to receive a message containing the token:
// rp.com
window.open('https://ip.com/doAuth?', '_blank');
window.addEventListener("message", (event) => {
if (event.origin !== "https://ip.com") return;
finalizeLoginWithToken(event.data.authToken);
// ...
},
false
);
When the authentication completes in the popup, the IP sends a message to the RP containing the token:
// ip.com
function returnTokenToRelyingParty(sRPOrigin, sToken){
window.opener.postMessage({'authToken': sToken}, sRPOrigin);
}
Similar to the postMessage approach, an IP site can use HTML5’s Broadcast Channel API to send messages between all of its contexts no matter where they appear. Unlike postMessage
(which can pass messages beween any origins), a site can only use Broadcast Channel to send messages to its own origin. BroadcastChannel is widely supported in modern browsers, but unlike postMessage
, it is not available in Internet Explorer.
You can see approaches #3 and #4 in use in a simple Demo App.
Click the Log me in! (Partitioned) button in Chromium 114+ and you’ll see that the subframe doesn’t “see” the cookie that is present in the WebDbg.com popup:
Now, click the postMessage(token) to RP button in that popup and it will post a message from the popup to the frame that launched it, and that frame will then store the auth token in a cookie inside its own partition:
We’ve now used postMessage
to explicitly share the auth token between the two IP contexts even though they are loaded within different cookie partitions.
The approaches outlined in this post avoid breakage caused by various current and future browser settings and privacy lockdowns. However, there are some downsides:
-Eric