How To Prepare Your IdentityServer For Chrome’s SameSite Cookie Changes – And How To Deal With Safari, Nevertheless

First, the good news: In February 2020 Google is going to release Chrome 80. This release will include Google's implementation of 'Incrementally better Cookies', which will make the web a more secure place and helps to ensure better privacy for users.

In diesem Artikel:

sg
Sebastian Gingter ist Consultant und „Erklärbär“ bei Thinktecture. Seine Spezialgebiete sind Backend-Systeme mit .NET Core und IdentityServer.

The bad news is that this new implementation is a breaking change in how the browser decides how to send cookies to servers. First of all, if you use separate domains for your web applications and your authentication server it is very likely that this change in Chrome will break the session experience for a portion of your users. The second problem is that it could also make it impossible for parts of your users to correctly log out of your system again.

 

What is this SameSite thing about, in the first place?

The web is a very open platform: When cookies were designed almost two decades ago and also when that design was revisited in 2011 in RFC 6265, Cross Site Request Forgery (CSRF) attacks and excessive user tracking weren’t a big thing yet.

In short, the normal cookie specification says that if a cookie is set for a specific domain, it will be sent to that domain with every request the browser makes. No matter if you directly navigate to that domain, if the browser just loads a resource (i.e. an image) from that domain, sends POST requests to it or embeds a part of it in an iframe. But maybe for the latter possibilities you don’t want the browser to automatically send the users session cookie to your server, as this would allow any website to execute JavaScript that executes requests against your server in the context of that user, without them noticing.

To prevent that, the SameSite cookie specification was drafted in 2016. It gives you much more control over when cookies should or should not be sent: When you set a cookie, you can now specify explicitly for each cookie when the browser should add it to the request. For that, it introduced the notion of same-site cookies when the browser is on your own domain and cross-site cookies when the browser is navigating a different domain but sends requests to your domain.

To be backwards-compatible, the default for same-site cookies did not change the previous behavior. You had to opt-in to that new feature and explicitly set your cookies to SameSite=Lax or SameSite=Strict to make them more secure. This has been implemented in .NET Framework and in all common browsers. Lax means, that cookies will be sent to the server on initial navigations, Strict means that the cookies will only be sent when you already were on that domain (i.e with the second request after initial navigation).

Sadly, this new feature was only slowly adopted (only 0.1% of all cookies handled on Chrome world-wide were using the SameSite flag, based on Chrome’s telemetry data in March 2019 [Source]).

Google decided to push adoption of that feature. To enforce that, they decided to change the default in the worlds most-used browser: Chrome 80 will require a newly specified setting SameSite=None to keep the old way of handling cookies, and if your omit the SameSite field like the old spec suggested, it will treat the cookie as set with SameSite=Lax.

Please note: The setting SameSite=None will only work if the cookie is also marked as Secure and requires a HTTPS connection.

Update: If you want more background information about SameSite cookies, there is a new article with all the nitty gritty details.

Does this affect me? And if yes, how?

If you have a single-page web application (SPA) that authenticates against an Identity Provider (IdP, for example IdentityServer 4) that is hosted on a different domain, and that application uses the so-called silent token refresh, you are affected.

When logging into the IdP, it will set a session cookie for your user, and that cookie comes from the IdP domain. At the end of the authentication flow your application, which comes from a different domain, receives some sort of access tokens, which are usually not very long-lived. When that token expires the application can’t access the resource server (API) anymore, and it would be a very bad user experience if the user had to log in again every time that happens.

To prevent that, you can use the silent token refresh. In that case the application creates an iframe that is not visible to the user, and starts the authentication process again in that iframe. The website of the IdP is loaded in the iframe, and if the browser sends the session cookie along the IdP recognizes the user and issues a new token.

Now the iframe lives in your SPA hosted on your application’s domain, and its content comes from the IdP domain. This is considered a cross-site request, so Chrome 80 will only send that cookie from the iframe to the IdP if the cookie explicitly states SameSite=None. If that is not the case, your silent token refresh will break in February when Chrome 80 ships.

There are also other scenarios that might be problematic for you: First, if you embed elements in your web application or site that originate from another domain, for example videos, and these need cookies to function properly, for example autoplay settings, these also will need to have the SameSite policy set. The same applies if your application needs to request 3rd party APIs from the browser that rely on cookie authentication.

Note: Obviously you can only change the cookie behavior of the cookies set by your own server. If you happen to use elements from other domains that are not under your control, you need to contact the 3rd party and ask them to change their cookies if there is an issue with them.

Fine, I'll change my code and set SameSite to None. I'm fine now, right?

Unfortunately not: Safari sadly has a „bug“. This bug results in Safari not recognizing the freshly introduced value None as a valid value for the SameSite setting. When Safari encounters an invalid value it treats this as if SameSite=Strict was specified, and will not send the session cookie to the IdP. This bug is fixed in Safari 13 on iOS 13 and macOS 10.15 Catalina, but it will not be back-ported to macOS 10.14 Mojave and on iOS 12, which have still a very big user base.

So, we’re caught between two stools now: Either we omit the SameSite policy and our Chrome users can’t do silent refresh, or we set SameSite=None and lock out the iPhone, iPad and Mac users that didn’t update or even are on older devices and can’t update to the latest iOS and macOS version.

Is there a way to know for sure that I am affected?

Luckily, yes. If you already have SameSite=None set, you probably already will have noticed that your application or web site does not work as expected in Safari on iOS 12 and macOS 10.4. If not, make sure to test your application or web site in these versions of Safari.

If you don’t set the SameSite value at all, you can simply open your application in Chrome and open the developer tools. You will see this warning:

				
					A cookie associated with a cross-site resource at {cookie domain} was set without the `SameSite` attribute.
A future release of Chrome will only deliver cookies with cross-site requests if they are set with `SameSite=None` and `Secure`.
You can review cookies in developer tools under Application>Storage>Cookies and see more details at
https://www.chromestatus.com/feature/5088147346030592 and
https://www.chromestatus.com/feature/5633521622188032.
				
			

If you already set SameSite=None but miss the Secure flag, you will get this warning:

				
					A cookie associated with a resource at {cookie domain} was set with `SameSite=None` but without `Secure`.
A future release of Chrome will only deliver cookies marked `SameSite=None` if they are also marked `Secure`.
You can review cookies in developer tools under Application>Storage>Cookies and
see more details at https://www.chromestatus.com/feature/5633521622188032.
				
			

So, how can I really fix this? I need both Chrome and Safari to work.

We, that is my colleague Boris Wilhelms and myself, did some research on that topic and found and verified a solution. There is also a good blog post from Microsoft’s Barry Dorrans on this issue. The solution isn’t beautiful and sadly requires browser sniffing on the server side, but it’s an easy fix and during the last weeks we already have successfully implemented that in several of our customers projects.

To solve the issue, we first need to make sure that the cookies that need to be transmitted via cross site requests – like our session cookie – is set to SameSite=None and Secure. We needed to find the options of that cookie in the projects code and adjust it accordingly. This fixed the issue with Chrome and introduced the Safari problem.

Then we added the following class and code snippets to the project. This adds and configures a cookie policy in ASP.NET Core web application. This policy will check if a cookie with SameSite=None should be set. If that is the case, it will then check the user agent of the browser and determine if this is a browser that has a problem with that setting like our affected Safari version. If that is the case too, it will set the cookies SameSite value to unspecified, which in turn will prevent setting SameSite at all, recreating the current default behavior for these browsers.

Please note: The solution presented here is for .NET Core. For full .NET Framework-based projects you need one of the versions that is specified in Barry Dorran’s post.

The class to add to your project

 
				
					using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
 
namespace Microsoft.Extensions.DependencyInjection
{
   public static class SameSiteCookiesServiceCollectionExtensions
   {
      /// <summary>
      /// -1 defines the unspecified value, which tells ASPNET Core to NOT
      /// send the SameSite attribute. With ASPNET Core 3.1 the
      /// <seealso cref="SameSiteMode" /> enum will have a definition for
      /// Unspecified.
      /// </summary>
      private const SameSiteMode Unspecified = (SameSiteMode) (-1);
 
      /// <summary>
      /// Configures a cookie policy to properly set the SameSite attribute
      /// for Browsers that handle unknown values as Strict. Ensure that you
      /// add the <seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" />
      /// into the pipeline before sending any cookies!
      /// </summary>
      /// <remarks>
      /// Minimum ASPNET Core Version required for this code:
      ///   - 2.1.14
      ///   - 2.2.8
      ///   - 3.0.1
      ///   - 3.1.0-preview1
      /// Starting with version 80 of Chrome (to be released in February 2020)
      /// cookies with NO SameSite attribute are treated as SameSite=Lax.
      /// In order to always get the cookies send they need to be set to
      /// SameSite=None. But since the current standard only defines Lax and
      /// Strict as valid values there are some browsers that treat invalid
      /// values as SameSite=Strict. We therefore need to check the browser
      /// and either send SameSite=None or prevent the sending of SameSite=None.
      /// Relevant links:
      /// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
      /// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
      /// - https://www.chromium.org/updates/same-site
      /// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
      /// - https://bugs.webkit.org/show_bug.cgi?id=198181
      /// </remarks>
      /// <param name="services">The service collection to register <see cref="CookiePolicyOptions" /> into.</param>
      /// <returns>The modified <see cref="IServiceCollection" />.</returns>
      public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services)
      {
         services.Configure<CookiePolicyOptions>(options =>
         {
            options.MinimumSameSitePolicy = Unspecified;
            options.OnAppendCookie = cookieContext =>
               CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
            options.OnDeleteCookie = cookieContext =>
               CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
         });
 
         return services;
      }

      private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
      {
         if (options.SameSite == SameSiteMode.None)
         {
            var userAgent = httpContext.Request.Headers["User-Agent"].ToString();

            if (DisallowsSameSiteNone(userAgent))
            {
               options.SameSite = Unspecified;
            }
         }
      }
 
      /// <summary>
      /// Checks if the UserAgent is known to interpret an unknown value as Strict.
      /// For those the <see cref="CookieOptions.SameSite" /> property should be
      /// set to <see cref="Unspecified" />.
      /// </summary>
      /// <remarks>
      /// This code is taken from Microsoft:
      /// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
      /// </remarks>
      /// <param name="userAgent">The user agent string to check.</param>
      /// <returns>Whether the specified user agent (browser) accepts SameSite=None or not.</returns>
      private static bool DisallowsSameSiteNone(string userAgent)
      {
         // Cover all iOS based browsers here. This includes:
         //   - Safari on iOS 12 for iPhone, iPod Touch, iPad
         //   - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
         //   - Chrome on iOS 12 for iPhone, iPod Touch, iPad
         // All of which are broken by SameSite=None, because they use the
         // iOS networking stack.
         // Notes from Thinktecture:
         // Regarding https://caniuse.com/#search=samesite iOS versions lower
         // than 12 are not supporting SameSite at all. Starting with version 13
         // unknown values are NOT treated as strict anymore. Therefore we only
         // need to check version 12.
         if (userAgent.Contains("CPU iPhone OS 12")
            || userAgent.Contains("iPad; CPU OS 12"))
         {
            return true;
         }

         // Cover Mac OS X based browsers that use the Mac OS networking stack.
         // This includes:
         //   - Safari on Mac OS X.
         // This does not include:
         //   - Chrome on Mac OS X
         // because they do not use the Mac OS networking stack.
         // Notes from Thinktecture: 
         // Regarding https://caniuse.com/#search=samesite MacOS X versions lower
         // than 10.14 are not supporting SameSite at all. Starting with version
         // 10.15 unknown values are NOT treated as strict anymore. Therefore we
         // only need to check version 10.14.
         if (userAgent.Contains("Safari")
            && userAgent.Contains("Macintosh; Intel Mac OS X 10_14")
            && userAgent.Contains("Version/"))
         {
            return true;
         }

         // Cover Chrome 50-69, because some versions are broken by SameSite=None
         // and none in this range require it.
         // Note: this covers some pre-Chromium Edge versions,
         // but pre-Chromium Edge does not require SameSite=None.
         // Notes from Thinktecture:
         // We can not validate this assumption, but we trust Microsofts
         // evaluation. And overall not sending a SameSite value equals to the same
         // behavior as SameSite=None for these old versions anyways.
         if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
         {
            return true;
         }

         return false;
      }
   }
}

				
			

Configure and enable the cookie policy

To use this cookie policy, you need to add the following to your startup code:

				
					public void ConfigureServices(IServiceCollection services)
{
   // Add this
   services.ConfigureNonBreakingSameSiteCookies();
}
 
public void Configure(IApplicationBuilder app)
{
   // Add this before any other middleware that might write cookies
   app.UseCookiePolicy();

   // This will write cookies, so make sure it's after the cookie policy
   app.UseAuthentication();
}

				
			

Okay. Am I done now?

Except for thorough testing, especially with Chrome 79 with the activated „SameSite by default cookie“ flag and the affected Safari versions on macOS and iOS, yes. You should be fine now. To test this in Chrome 79 navigate to chrome://flags, search for samesite and enable the SameSite by default cookies flag. Relaunch the browser and you can test the upcoming changes right now.

Seriously: Make sure that your silent refresh – or generally your cross-site requests that need cookies – still work on these devices and browsers.

Can't I simply wait for my authentication server vendor to fix that for me?

This is unlikely. In our concrete example case here it is actually not the IdentityServer itself that manages the cookies. IdentityServer relies on the ASP.NET Core framework’s builtin authentication system, and this is where the session cookies are managed. While the ASP.NET Core framework has been updated to support both the new SameSite value None and a technical setting Unspecified (not sending SameSite at all), Microsoft said they cannot introduce user agent sniffing in ASP.NET Core directly. So this really is up to you and your existing project.

Summary

Chrome will soon (February 2020) change its default behavior of handling cookies. In the future it will require the SameSite flag to be set explicitly to None and the Secure flag to be set too, to allow the cookie to be added to certain cross-site requests. If you do that, common versions of Safari will barf about that.

To make sure all browsers are happy, you set all of the affected cookies to Secure and SameSite=None, and then you add a cookie policy (code shown above) that can override these settings and remove the SameSite flag again for browsers that don’t interpret the None value correctly.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Database Access with Sessions
.NET
KP-round

Data Access in .NET Native AOT with Sessions

.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
Old computer with native code
.NET
KP-round

Native AOT with ASP.NET Core – Overview

Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
.NET
KP-round

Optimize ASP.NET Core memory with DATAS

.NET 8 introduces a new Garbage Collector feature called DATAS for Server GC mode - let's make some benchmarks and check how it fits into the big picture.
09.10.2023
.NET CORE
pg

Incremental Roslyn Source Generators: High-Level API – ForAttributeWithMetadataName – Part 8

With the version 4.3.1 of Microsoft.CodeAnalysis.* Roslyn provides a new high-level API - the method "ForAttributeWithMetadataName". Although it is just 1 method, still, it addresses one of the biggest performance issue with Source Generators.
16.05.2023
AI
favicon

Integrating AI Power into Your .NET Applications with the Semantic Kernel Toolkit – an Early View

With the rise of powerful AI models and services, questions come up on how to integrate those into our applications and make reasonable use of them. While other languages like Python already have popular and feature-rich libraries like LangChain, we are missing these in .NET and C#. But there is a new kid on the block that might change this situation. Welcome Semantic Kernel by Microsoft!
03.05.2023
.NET
sg

.NET 7 Performance: Regular Expressions – Part 2

There is this popular quote by Jamie Zawinski: Some people, when confronted with a problem, think "I know, I'll use regular expressions." Now they have two problems."

In this second article of our short performance series, we want to look at the latter one of those problems.
25.04.2023