Are you operating a web application with sessions (e.g. for saving user preferences, shopping carts)? Do you allow users to sign in using identity providers such as Google and Apple, or use solutions such as Auth0?

What do those scenarios have in common? They both rely on cookies to achieve their functionality. What you need to know is that browser cookie behavior changes coming as early as February next year may break your user experience. Google Chrome is going to be the first browser vendor to roll out a change that might not be compatible with your web application.

What Might Be Affected?

Here's a list of the scenarios that are most likely to be affected by the changes:

  • Integrations with Identity Providers using protocols such as SAML 2.0 or OpenID Connect.
  • Embedding web application content from a third-party domain.
  • Querying APIs from a third-party domain.
  • Note: this is not an exhaustive list

Let's look at some of those scenarios in more detail and, in particularwhat aspects, will break under the new browser behaviors.

Web Application Sign-In Using Identity Providers

When a web application implements Sign-In using OIDC, it will engage in a series of redirects meant to send the user to authenticate with the provider and come back with proof of authentication. That proof of authentication is represented by the ID Token sent back to the app, as shown in step (D) of the diagram below.

Before:

Web Application Sign-In using Identity providers before changes

After:

Web Application Sign-In using Identity providers after changes

During step (D) in the image above, the web application performs ID Token validation for which it needs information stored in the session. Without any updates to today's implementations, the cookie carrying the session or authentication request binding info (nonce in the diagram) would no longer be attached to the POST, resulting in the failure of the response validation checks and, ultimately, the inability of the end-user to sign in the app.

Obviously, there are more details, conditions, and nuances to it. We’ll dive into the technicalities as we go along.

Why Is Google Pushing These Changes?

In short, to provide a more secure default mode of function and to open up possibilities for better privacy controls in the future.

Cookies today, in their default configuration, are the reason behind many web applications’ CSRF vulnerabilities. Such a vulnerability is where an HTTP form is submitted from the attacker’s context to an application that uses the attached POST request cookie as a mechanism to identify the end-user’s session and executes a malicious action; e.g. executing a money transfer to the attacker.

The coming update changes the cookies’ default configuration from a less secure model to one which strikes a reasonable balance between security and usability. With the changes applied, the attack above would not work because the HTTP Form POST is done from a different origin and the cookie will, by default, not be attached and the action will fail due to missing authentication.

This change allows developers to be protected by default, but still allows applications to opt in for the less-secure mode, should they need it.

When?

When the changes land in your user’s browser. The actual date will depend on the browser vendor, version and release channel they follow.

Chrome has announced these changes in their web.dev blog post and now tracks them here and here. They are available behind chrome flags today, and will ship enabled by default in a stable release channel with Chrome 80 and in a beta release channel Chrome 78.

Firefox already has these implemented with Firefox 69 behind a developer preference flag but has given no target release version for enabling it by default.

Edge has announced support with an upcoming new Edge version, but no ETA has been given on that yet.

Safari has not signaled adoption yet.

Summary

  • Chrome 80 February 2020 (beta channel earlier, canary channel already today behind an opt-in flag).
  • Firefox 69 already released this behind a developer preference flag. No release date for public release.
  • Edge — Adoption announced for the new Edge version.
  • Safari — No adoption signal yet.
  • Others — No adoption signal yet.

What to Do?

Reach out to your technical partners, providers, and service operators and ask them if they’re ready for this change. That goes for both services you may be consuming as well as SDKs you’re using to consume them.

If you’re a customer of Auth0, then be sure we’re already executing steps necessary to facilitate this change. Some changes will happen automatically, on Auth0’s side, without requiring any action on yours. Depending on the particular SDK and underlying grant your web app is using to implement Sign-In and/or Authorization, you might need to update to a newer version of the SDK capable of handling the new browser behavior.

Check and update your SDKs periodically, and be on the lookout for any advisories in their README and CHANGELOG as well as your Auth0 Dashboard Notifications for any actions required.

What Is This Change?

There's not a web developer on earth that wouldn't ever encounter cookies. Cookies, as an HTTP State Management Mechanism, were standardized by IETF all the way back in 1997.

Since then, this mechanism has been evolving (2000, 2011) with the latest update being in draft state since 2016.

Developers are taught, through infinite online material, that cookies will be attached to all requests to their web service by default and that the browser takes care of that. That is only partly true when a cookie attribute called SameSite is involved. Ever since this attribute was introduced through one of the draft updates in 2017, its default value was not breaking existing web services. It was still attaching cookies to all requests that are in scope. That’s what is now changing.

What Is SameSite Anyway?

SameSite is a cookie attribute, defined like so

The SameSite attribute limits the scope of the cookie such that it will be attached to requests only if those requests are same-site, as defined by the algorithm in Section 5.2 of this document.

Other cookie attributes you may already be familiar with include:

  • HttpOnly: makes the browser attach cookies only to HTTP requests and not be readable using javascript’s document.cookie interface.
  • Secure: makes the browser attach cookies only to a secure context, meaning HTTP over TLS (https).
  • Max-Age / Expires: controls whether cookies are bound to browsing session /dropped when the browser terminates the browsing session/, or are "persistent" /persists browsing session termination/.

Omitting cookies set using the browser’s javascript API, these options are provided by the server with its HTTP responses as part of the Set-Cookie header(s). The browser, upon receipt of the response, parses these and maintains its cookie jar.

Here’s how a regular server-side Set-Cookie header works.

Here’s a fresh interaction. The end-user requests a page he has not visited before.

How a regular server-side Set-Cookie header works fresh interaction

The server wishes to change the way it renders the next time, so it sets a “seen” cookie. The grey part of the set-cookie header is the actual cookie key+value; the red part is all cookie attributes the browser stores internally in its cookie jar to be able to decide later on if it includes the cookie key+value pair in its requests.

Now, let’s make that request again in the same browsing session.

How a regular server-side Set-Cookie header works making a request again in the same browsing session

A request is being made to the same server, and since the cookie attributes do not prohibit the “seen” cookie from being sent, it is automatically included as a cookie header in the request. The server can now respond differently based on the fact that such a cookie has been received.

In the example Set-Cookie header above path=/ and httponly are cookie attributes, just like SameSite, which today [today is important when it comes to incompatibility we’ll look at later] has three defined values:

SameSite=Strict

The intent behind the SameSite=Strict value is CSRF mitigation/protection in strict mode; otherwise, eligible cookies are sent only when the origin (not Origin as defined by RFC6454) of the requesting page is the same as one of the resources it is accessing.

This means that when the user navigates a link from another site (e.g. via a link pointing to yours), your cookies do not get attached and therefore any previously established cookie-based sessions are not going to be loaded. It won’t be until the user navigates a link within the origin of your page that the browser attaches the cookies.

Here is the scenario in which Strict cookies are not vetted from being attached.

A scenario in which Strict cookies are not vetted from being attached.

The user is already at www.example.com and clicks a /resource link. If this was an XHR request from the same origin, it would also be attached.

Here are two scenarios in which SameSite=Strict cookies are prevented from being attached.

Here is a scenario in which SameSite=Strict cookies are vetted from being attached.

Here is another scenario in which SameSite=Strict cookies are vetted from being attached

The user is not currently browsing www.example.com and clicks a www.example.com/resource link or somehow submits a form. If these were XHR requests, they would also not be attached. Notice the cookie header is not being sent by the browser because the request does not match the criteria of a SameSite=Strict cookie.

SameSite=Lax

The SameSite=Lax setting has the exact same semantics as Strict but excludes top-level redirects from the restrictions to allow regular “browsing” behavior. This means a cookie still won’t be attached with iframes, XHR Request to an API or a posted HTTP form from another origin, but it will be attached when an end-user clicks a regular link, top-level web page triggers window.location = to redirect or a GET request is started as a result of a 3xx (300 range HTTP response status code).

Here are the scenarios in which SameSite=Lax cookies are not vetted from being attached.

Here is a scenario in which SameSite=Lax cookies are not vetted from being attached.

Here is another scenario in which SameSite=Lax cookies are not vetted from being attached.

The user is already at www.example.com and clicks a /resource link, or they are at another website and click a link to www.example.com/resource.

Same as with SameSite=Strict: if the request is not from the same origin or is not a top-level redirect (allowed by lax), an HTTP form posted from another origin will not have cookies attached to the request.

SameSite=None

The semantics of SameSite=None today are the same as not providing SameSite at all. As a matter of fact, it wasn’t even a recognized value until recently. It is the behavior developers are familiar with; in short, request goes to my web page, cookies are attached, regardless of the request’s origin or type (XHR, redirect, iframe, top-level navigation…).

Unfortunately, the value None was not officially defined before the RFC draft update from April 27, 2019. While it has been accepted by fast-moving browsers such as Chrome and Firefox, this value’s adoption isn’t without issues, causing headaches for developers when it comes to preparing for the upcoming Chrome 80 changes.

How Is SameSite Changing?

It is expected that, starting ~February 2020 (Chrome 80, exact date unknown at this time), unless provided explicitly, the browser will interpret lack of an explicitly set SameSite attribute as if the cookie carried a SameSite attribute’s value Lax, instead of today's behavior of value None. In addition, only cookies marked with the Secure attribute are allowed to have SameSite attribute’s value None.

If you are not setting the SameSite attribute today, Chrome 80 will only attach these cookies to requests originating from your own site and top-level GET requests (such as redirects). They will no longer be sent from other origins when embedded (iframe) or with AJAX (XHR) requests.

If you are setting the SameSite attribute’s value None today and it is not also marked as Secure, Chrome 80 will reject this cookie completely.

Mozilla Firefox also indicated the intention to ship these new behaviors sometime in 2020, behind a user preference at first.

If SameSite=None cross-origin behaviors are needed for your web service to function, keep on reading. There is a very high chance some of your end-users are using browsers that do not properly support this attribute’s value, so that setting the cookie attribute will result in various undesired behaviors.

Incompatible Browsers

Wouldn’t it be great if you could simply set SameSite=None and call it a day? It would. Unfortunately, the history behind the SameSite attribute and, until recently, a bug in the WebKit browser engine require an intermediate solution to be deployed to make sure no end-user is affected by this change.

First, the original definition of the SameSite attribute included the following normative requirement:

If the SameSite attribute's value is neither of these [editor: at the time defined values Strict or Lax], the cookie will be ignored.

In August 2017, the behavior was changed so that only the unrecognized SameSite attribute’s values are to be ignored and instead the value None be applied.

This means that any ~2+ years old browser conforming to the specification will simply reject your SameSite=None cookie and won’t ever attach it to any requests.

Second, WebKit, the browser engine used by Safari on macOS, iOS, and iPadOS, but also all browsers on the iOS/iPadOS platforms (Chrome, Firefox, etc. download from the AppStore), has a bug in it, which is setting SameSite=Strict instead of SameSite=None when provided.

The WebKit bug has been fixed with iOS 13 but will, unfortunately, not get backported to older iOS major releases. The bug fix propagation into macOS or macOS Safari versions is also not clear.

Therefore, if your cookies need the SameSite attribute’s value None related properties, you need to work around the incompatible user-agents.

Working around incompatible browsers

There are different levels of incompatibility. Some browsers reject the cookie with SameSite=None completely; some apply the value Strict instead.

At first, it seemed like User-Agent (UA) Sniffing (reading and parsing the User-Agent HTTP header) could be used to detect the incompatible browsers and skip setting the SameSite attribute altogether for them. Well, that’s proving to be far too complex and brittle to achieve, since we’re not only targeting browser vendor versions but also engines. Let’s keep looking for a universal approach.

We could use UA Sniffing to detect Chrome 80+ and only apply SameSite=None for that, right? Again, not as simple. Due to the WebKit bug forever present on iOS 12, you’d also need to factor in the operating system major version and the rendering engine. Plus, other browser vendors are already signaling that they’ll be adopting these changes. In time, when this becomes an official standard, all browsers will. We’re seeking a solution with few drawbacks that works on all browsers, regardless of their age, vendor, operating system, or rendering engine.

The recommended workaround from Google and one we're taking internally, does not depend on brittle UA Sniffing but rather, setting two cookies, one with the SameSite attribute’s value None; the other one, without any SameSite attribute whatsoever.

This is proven to work 100% of the time since, until Chrome 80, we had no reason to set cookies with the SameSite attribute to get its None value properties. This was the default. How does that look in practice?

The HTTP response that sets cookies via the Set-Cookie header would create a pair of cookies for each that is supposed to have the SameSite=None properties, like so

HTTP/1.1 200 OK
Date: Fri, 11 Oct 2019 09:50:07 GMT
Content-Type: text/html; charset=utf-8
Set-Cookie: cookieName=value; SameSite=None; Secure
Set-Cookie: cookieName-legacy=value
Connection: Close


<html>
 ... content
</html>

When retrieving a cookie value, e.g. using the Node.js Koa framework you would do the following

app.use(async (ctx, next) => {
  let cookie = ctx.cookies.get('cookieName');
  if (cookie === undefined) {
    cookie = ctx.cookies.get('cookieName-legacy');
  }

  // proceed to work with cookie
  // ...

  await next();
});

Using the Node.js Express framework the code is very similar

app.use((req, res, next) => {
  let cookie = req.cookies['cookieName'];
  if (cookie === undefined) {
    cookie = req.cookies['cookieName-legacy']
  }

  // proceed to work with cookie
  // ...

  return next();
});

Alternatively, one may choose to detect the browser vendor and/or operating system via the User-Agent header string at the point of setting the Set-Cookie header. Refer to the list of incompatible clients to see what it takes to accomodate them all. Note, you may be forced to take on this approach if you'd run into cookie size and cookies per domain limits, that however depends on what is it that you store in your cookies - is it just references or arbitrary sized serialized objects?

What’s Next?

From the nature of the change, it is already clear some SDKs, depending on the grant they execute, are not affected; namely:

As already mentioned, if you’re a customer of Auth0, then be sure we’ve got you covered. We already have changes in progress on the service side to get rid of those pesky Javascript console warnings.

We’re also going through our SDK libraries, reviewing their use of cookies and making sure we update those that need to be updated well before this change goes into effect for regular users. So, again, be sure to check and update your SDKs periodically and be on the lookout for any advisories in their README and CHANGELOG as well as your Auth0 Dashboard Notifications for any actions required.

We will do our best to update this blog post as new developments happen.