Skip to main content

.NET 4.7.2 and 4.8 supports the 2019 draft standard for SameSite since the release of updates in December 2019. Developers are able to control the value of the SameSite attribute in code using the HttpCookie.SameSite property. Setting the SameSite property to Strict, Lax, or None results in those values being written on the network with the cookie. Setting it equal to (SameSiteMode)(-1) indicates that no SameSite attribute should be included on the network with the cookie.

// ASP.NET Same Site Cookie Samples
// https://github.com/blowdart/AspNetSameSiteSamples

// ------------------------------------------------------------------------------------------------
// AspNetCore31RazorPages/Startup.cs
// https://github.com/blowdart/AspNetSameSiteSamples/blob/master/AspNetCore31RazorPages/Startup.cs
// ------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AspNetCore31RazorPages
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
                options.OnAppendCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
                options.OnDeleteCookie = cookieContext => CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
            });

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.SameSite = SameSiteMode.None;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
                    options.Cookie.IsEssential = true;
                });

            services.AddSession(options =>
            {
                options.Cookie.SameSite = SameSiteMode.None;
                options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
                options.Cookie.IsEssential = true;
            });

            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");

                // The default HSTS value is 30 days.
                // You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseCookiePolicy();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseSession();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }

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

                if (SameSite.BrowserDetection.DisallowsSameSiteNone(userAgent))
                {
                    options.SameSite = SameSiteMode.Unspecified;
                }
            }
        }
    }
}

//
// Fixing the problem
//
// Microsoft's approach to fixing the problem is to help you implement browser
// sniffing components to strip the sameSite=None attribute from cookies if a
// browser is known to not support it. Google's advice was to issue double
// cookies, one with the new attribute, and one without the attribute at all
// however we consider this approach limited; some browsers, especially mobile
// browsers have very small limits on the number of cookies a site, or a domain
// name can send, and sending multiple cookies, especially large cookies like
// authentication cookies can reach that limit very quickly causing application
// breaks that are hard to diagnose and fix. Furthermore as a framework there is
// a large ecosystem of third party code and components that may not be updated
// to use a double cookie approach.
//
// The browser sniffing code used in the sample projects in this solution is
// contained in two files
//
// These detections are the most common browser agents we have seen that support
// the 2016 standard and for which the attribute needs to be completely removed. It
// is not meant as a complete implementation, your application may see browsers
// that our test sites do not, and so you should be prepared to add detections as
// necessary for your environment.
//
// How you wire up the detection varies according the version of .NET and the web
// framework that you are using.

namespace SameSite
{
    // Filter same site none for incompatible user agents
    public static class BrowserDetection
    {
        // Same as https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
        public static bool DisallowsSameSiteNone(string userAgent)
        {
            if (string.IsNullOrEmpty(userAgent))
            {
                return true;
            }

            // Note that these detections are a starting point. See https://www.chromium.org/updates/same-site/incompatible-clients for more detections.

            // 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
            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.
            if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
                userAgent.Contains("Version/") && userAgent.Contains("Safari"))
            {
                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.
            if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
            {
                return true;
            }

            // Unreal Engine runs Chromium 59, but does not advertise as Chrome until 4.23. Treat versions of Unreal
            // that don't specify their Chrome version as lacking support for SameSite=None.
            if (userAgent.Contains("UnrealEngine") && !userAgent.Contains("Chrome"))
            {
                return true;
            }

            return false;
        }

        public static bool AllowsSameSiteNone(string userAgent)
        {
            return !DisallowsSameSiteNone(userAgent);
        }
    }
}