Original Author: Sebastian Gingter
Original Link: https://www.thinktecture.com/en/identityserver/prepare-your-identityserver/
Translator: Desert Wolf at the End
Translation Link: https://dotnet9.com/2022/04/How-To-Prepare-Your-IdentityServer-For-Chromes-SameSite-Cookie-Changes-And-How-To-Deal-With-Safari-Nevertheless
This article is a share from the author in 2019. Some of the viewpoints and technologies mentioned are still effective for our current development. It is recommended to read the original text. If you have any questions about this translation, feel free to submit a PR.
First, the good news: Google will include their implementation of "Incrementally Better Cookies" when releasing Chrome 80 in February 2020, making the web a safer place and helping ensure better privacy for users (Editor's note: It is now April 28, 2022, and Chrome has released multiple updates).
The bad news is that this new implementation represents a significant change in how browsers decide to send cookies to servers. Firstly, if you use separate domains for your web application and authentication server, this change in Chrome is likely to break the session experience for some users. The second issue is that it may also prevent some users from properly logging out of your system.
1. First, what is this SameSite all about?
The web is a very open platform: Cookies were designed about 20 years ago, and when the design was revisited in RFC 6265 in 2011, Cross-Site Request Forgery (CSRF) attacks and excessive user tracking were not major issues.
In short, the normal cookie specification says that if a cookie is set for a specific domain, it will be sent with every request the browser makes to that domain—whether you navigate directly to that domain, or the browser simply loads a resource (e.g., an image) from it, sends a POST request to it, or embeds part of it in an iframe. But for the latter possibility, you probably don't want the browser to automatically send the user's session cookie to your server, as this would allow any website to execute JavaScript that sends requests to your server in the context of that user without their knowledge.
To prevent this, the SameSite cookie specification was drafted in 2016. It gives you better control over when cookies should or shouldn't be sent: When you set a cookie, you can now explicitly specify when the browser should include it in a request. To this end, it introduces the concept of a same-site cookie when the browser is on your own domain, and a cross-site cookie when the browser navigates on a different domain but sends a request to your domain.
For backward compatibility, the default setting for same-site cookies did not change previous behavior. You had to opt into this new feature and explicitly set your cookie to SameSite=Lax or SameSite=Strict to make it more secure. This has been implemented in .NET Framework (including .NET Core) and all common browsers. Lax means the cookie will be sent to the server on initial navigation, while Strict means the cookie will only be sent when you are already on that domain (i.e., the second request after initial navigation).
Unfortunately, adoption of this new feature has been slow (according to Chrome telemetry from March 2019 source, only 0.1% of all cookies handled by Chrome worldwide used the SameSite flag).
Google decided to drive adoption. To enforce it, they changed the default behavior of the world's most used browser: Starting with Chrome 80, you must specify a new setting SameSite=None to preserve the old way of handling cookies. If you omit the SameSite field as the old specification suggested, the cookie will be treated as if it was set with SameSite=Lax.
Note: The setting SameSite=None is only effective if the cookie is also marked Secure and requires an HTTPS connection.
Update: If you want more background on SameSite cookies, there is a new article with all the details.
2. Does this affect me? If so, how?
You are affected if you have a single-page web application (SPA) that authenticates against an identity provider (IdP, e.g., IdentityServer 4) hosted on a different domain, and the application uses so-called silent token refresh.
When logging into the IdP, it sets a session cookie for your user that originates from the IdP domain. At the end of the authentication flow, the application from a different domain receives some kind of access token that typically does not last long. When that token expires, the application can no longer access the resource server (API). It would be a very poor user experience if the user had to log in again every time this happens.
To prevent this, you can use silent token refresh. In this scenario, the application creates an iframe invisible to the user and starts the authentication process again within that iframe. The IdP's website loads in the iframe, and if the browser sends the session cookie along with the IdP, it recognizes the user and issues a new token.
Now the iframe exists in the SPA hosted on the application domain, its content coming from the IdP domain. Chrome 80 will only send that cookie from the iframe to the IdP if the cookie explicitly states SameSite=None, as this is considered a cross-site request. If this is not the case, your silent token refresh will break when Chrome 80 is released in February.
There are other scenarios that might cause you trouble: For instance, if you embed elements from another domain in your web application or website, such as autoplay settings for videos, and those require cookies to function properly, they will also need to have the SameSite policy set. The same applies if your application needs to request a third-party API from the browser that relies on cookie-based authentication.
Note: Obviously, you can only change the cookie behavior of your own server regarding cookie settings. If you happen to use elements from other domains that you don't control, you need to contact the third party and ask them to change their cookies if there is a problem.
3. Okay, I'll change my code and set SameSite to None. I'm good now, right?
Unfortunately, Safari has a "bug". This bug causes Safari to fail to recognize the newly introduced value None as a valid value for the SameSite setting. When Safari encounters an invalid value, it behaves as if SameSite=Strict was specified, and it won't send the session cookie to the IdP. This bug was fixed in Safari 13 on iOS 13 and macOS 10.15 Catalina, but it will not be backported to macOS 10.14 Mojave and iOS 12, which still have a very large user base.
So, we are now in a dilemma: Either we ignore the SameSite policy and our Chrome users cannot do silent refresh, or we set SameSite=None and lock out iPhone, iPad, and Mac users who cannot update to the latest versions of iOS and macOS due to older devices or restrictions.
4. Is there a way to determine if I'm affected?
Fortunately, yes. If you have already set SameSite=None, you might have noticed that your application or website doesn't work properly in Safari on iOS 12 and macOS 10.4. If not, make sure to test your application or website on those 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 the following 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 have already set SameSite=None but forgot to set the Secure flag, you will receive the following 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.
5. So, how do I actually solve this? I need Chrome and Safari to work properly.
We—my colleague Boris Wilhelms and I—did some research on this topic and found and validated a solution. Microsoft's Barry Dorrans also has a good blog post on this issue. The solution is not pretty and unfortunately requires browser sniffing on the server side, but it is a simple solution that we have successfully implemented in several of our client projects over the past few weeks.
To solve this, we first need to ensure that cookies that need to be transmitted via cross-site requests (such as our session cookie) are set to SameSite=None and Secure. We need to find the options for that cookie in the project code and adjust accordingly. This solves the Chrome problem but introduces the Safari problem.
Then we add the following class and code snippet to the project. This adds and configures a cookie policy in the ASP.NET Core web application. This policy will check if a cookie is set to SameSite=None. If so, it will check the browser's user agent and determine if this is a browser with problematic settings, like our affected Safari versions. If that is also the case, it will set the cookie's SameSite value to unspecified, which in turn will completely prevent setting SameSite, thereby recreating the current default behavior for those browsers.
Note: The solution provided here is for .NET Core. For full .NET Framework projects, you need to look at one of the versions specified in Barry Dorran's post.
5.1 Class to Add to the 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;
}
}
}
5.2 Configuring and Enabling 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();
}
6. Okay. Am I done now?
Apart from thorough testing, especially with the "SameSite by default cookies" flag activated in Chrome 79 and the affected Safari versions on macOS and iOS, yes, you should be good now. To test in Chrome 79, navigate to chrome://flags, search for samesite, and enable the SameSite by default cookies flag. Restart your browser and you can immediately test the upcoming changes.
Seriously: Make sure that your silent refresh—or generally any cross-site requests that require cookies—still work on these devices and browsers.
7. Can't I simply wait for my authentication server vendor to fix this for me?
That's unlikely. In our specific example here, it's not IdentityServer itself that actually manages the cookies. IdentityServer relies on the built-in authentication system of the ASP.NET Core framework, which is where session cookies are managed. While the ASP.NET Core framework has been updated to support the new SameSite value None and the technical setting Unspecified (to not send SameSite), Microsoft stated that they cannot introduce user-agent sniffing directly into ASP.NET Core. So it really depends on you and your existing project.
8. Summary
Chrome will soon (February 2020) change its default behavior for handling cookies. In the future, it will require that cookies intended to be sent with certain cross-site requests be explicitly set with SameSite=None and the Secure flag. If you do that, common versions of Safari will break.
To ensure all browsers are happy, set all affected cookies to Secure and SameSite=None, then add a cookie policy (like the code shown above) that overrides these settings and removes the SameSite flag again for browsers that cannot correctly interpret the value None.