SameSite in code for your ASP.net applications

This post has been republished via RSS; it originally appeared at: New blog articles in Microsoft Tech Community.

In order to compensate for the fact that older browsers do not understand the SameSite=None attribute on cookies and consider it equivalent to SameSite=Strict, in this last part of the articles on the SameSite cookie specification changes, I will show some demo code on how to issue the attribute on a per request basis.

 

This is valid only if we are targeting a SameSite=None attribute – the new default of SameSite=Lax will not need any per request code changes in your application logic, it is brought about by the November / December .Net Framework roll-ups directly. We will only focus on the edge cases where we need the cookie to have the SameSite=None attribute and also deal with requests that are incoming from older browsers. I will try and show code that addresses the scenarios demonstrated in the previous installment of the series.

 

Session state cookie with SameSite=None.

 

The session cookie is emitted during the Session_Start event handling logic. Hence, we can modify this logic to incorporate additional code to decorate the session cookie as needed. Here is how the Session_Start code would look like:

 

void Session_Start(object sender, EventArgs e) { // Code that runs when a new session is started //get the useragent for the request string currentUserAgent = HttpContext.Current.Request.UserAgent; //decide if we need to strip off the same site attribute for older browsers bool dissallowSameSiteFlag = DisallowsSameSiteNone(currentUserAgent); //get the name of the cookie, if not defined default to the "ASP.NET_SessionID" value SessionStateSection sessionStateSection = (SessionStateSection)ConfigurationManager .GetSection("system.web/sessionState"); string sessionCookieName; if(sessionStateSection != null) { //read the name from the configuration sessionCookieName = sessionStateSection.CookieName; } else { sessionCookieName = "ASP.NET_SessionId"; } //should the flag be positioned to true, then remove the attribute by setting //value to SameSiteMode.None if (dissallowSameSiteFlag) Response.Cookies[sessionCookieName].SameSite = (SameSiteMode)(-1); else Response.Cookies[sessionCookieName].SameSite = SameSiteMode.None; //while we're at it lets also make it secure if (Request.IsSecureConnection) Response.Cookies[sessionCookieName].Secure = true; }

 

The code starts out by making not of what the incoming request’s user agent is. This is a string that is sent in by each browser (or connecting application) identifying its type. You can check what common user agent strings browsers use in sites like www.useragentstring.com. We will then call a method that will indicate if we should emit or we should disallow the SameSite=None attribute on the cookie based on the provided user agent – the code for DisallowsSameSiteNone will be provided later.

 

We then have to know what the name of the Session cookie is: to do this, we look at the web.config file, where a custom name for the cookie can be specified. If no custom name is found, the code will default to the standard name of the cookie, which is ‘ASP.Net_SessionId”.

 

Based on the dissallowSameSiteFlag we either append the SameSite=None attribute to the cookie, or we omit appending the SameSite attribute altogether – by setting the SameSite enumeration to -1.

 

Finally, the code also appends the Secure attribute to the session cookie in both cases, when the SameSite attribute is present or when it is not. This is not necessary for the case when the SameSite attribute is not present, however it is required for cookies that have the SameSite attribute set to the value ‘None’.

 

Forms based Authentication

The code in this scenario is fairly similar to the code used on Session_Start for the session cookie. However, you would have to modify the authentication cookie based on the same logic inspecting the user agent of the connecting client. I will show a piece of sample code when using ASP.net Identity cookie authentication instead so you can also see how this would look like.

 

public void ConfigureAuth(IAppBuilder app) { ... //get the useragent for the request string currentUserAgent = HttpContext.Current.Request.UserAgent; //decide if we need to strip off the same site attribute for older browsers bool dissallowSameSiteFlag = DisallowsSameSiteNone(currentUserAgent); //decide the value of the attribute Microsoft.Owin.SameSiteMode sameSiteAttributeVal; if (dissallowSameSiteFlag) sameSiteAttributeVal = (Microsoft.Owin.SameSiteMode)(-1); else sameSiteAttributeVal = Microsoft.Owin.SameSiteMode.None; // Enable the application to use a cookie to store information for the signed in // user // and to use a cookie to temporarily store information about a user logging in // with a third party login provider // Configure the sign in cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login"), CookieDomain = activeHostName, // Set the SameSite based on the calculated value CookieSameSite = sameSiteAttributeVal, Provider = new OwinCookieAuthProvider { OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); ...

 

In the ConfigureAuth method of the Startup.Auth.cs file, we again start by getting the user agent of the connecting client and calculating if we should or should not emit the SameSite=None attribute on the authentication cookie, by calling the DissallowSameSiteNone(string) method – code to come below.

 

Based on the result, we either set the SameSite value to ‘None’ or we set it to -1, indicating that the SameSite attribute should not be emitted at all. At this point we are ready to call the UseCookieAuthentication extension method and pass in the CookieAuthenticationOptions object. Within this object we have a CookieSameSite property, which we will set to the value calculated beforehand.

 

Azure Active Directory Authentication.

 

The approach in scenarios where Azure Active Directory Authentication (AAD for short) is used is very similar to the code for the ASP.net Identity displayed above. The changed are again operated in the ConfigureAuth method of the Startup.Auth.cs file like the following:

 

public void ConfigureAuth(IAppBuilder app) { app.SetDefaultSignInAsAuthenticationType( CookieAuthenticationDefaults.AuthenticationType); app.UseCookieAuthentication( new CookieAuthenticationOptions { CookieSameSite = Microsoft.Owin.SameSiteMode.None }); ... app.UseOpenIdConnectAuthentication( new OpenIdConnectAuthenticationOptions { ClientId = clientId, Authority = authority, RedirectUri = replyUrl, PostLogoutRedirectUri = postLogoutRedirectUri, CookieManager = new SameSiteCookieManager(new SystemWebCookieManager()), Notifications = new OpenIdConnectAuthenticationNotifications() { // // If there is a code in the OpenID Connect response, // redeem it for an access token and refresh token, and // store those away. // AuthorizationCodeReceived = (context) => { var code = context.Code; ClientCredential credential = new ClientCredential( clientId, appKey); string signedInUserID = context.AuthenticationTicket.Identity.FindFirst( ClaimTypes.NameIdentifier).Value; AuthenticationContext authContext = new AuthenticationContext(authority, new ADALTokenCache(signedInUserID)); AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( code, new Uri( HttpContext.Current.Request.Url.GetLeftPart( UriPartial.Path)), credential, graphResourceId); return Task.FromResult(0); } } }) .UseWindowsAzureActiveDirectoryBearerAuthentication( new WindowsAzureActiveDirectoryBearerAuthenticationOptions { Tenant = ConfigurationManager.AppSettings["ida:Tenant"], TokenValidationParameters = new TokenValidationParameters { ValidAudience = ConfigurationManager.AppSettings["ida:Audience"] } }); // This makes any middleware defined above this line run before the // Authorization rule is applied in web.config app.UseStageMarker(PipelineStage.Authenticate); }

 

The first point to note is that we indicate that cookies related to authentication should be marked with SameSite=None when calling the UseCookieAuthentication extension method and passing it an instance of CookieAuthenticationOptions class.

 

The major change is that we append a CookieManager object that we instantiate and pass to the CookieManager property of the OpenIdConnectAuthenticationOptions class, an instance of which is itself passed into the UseOpenIdConnectAuthentication extension method. This allows the delegation of how the authentication related cookies are generated by the instance of the CookieManager class which we can code.

 

The code for the SameSiteCookieManager class can be found below:

 

public class SameSiteCookieManager : ICookieManager { private readonly ICookieManager _innerManager; public SameSiteCookieManager() : this(new CookieManager()) { } public SameSiteCookieManager(ICookieManager innerManager) { _innerManager = innerManager; } //method called when appending cookies to the response public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options) { CheckSameSite(context, options); _innerManager.AppendResponseCookie(context, key, value, options); } //method called when clearing cookies from the response public void DeleteCookie(IOwinContext context, string key, CookieOptions options) { CheckSameSite(context, options); _innerManager.DeleteCookie(context, key, options); } public string GetRequestCookie(IOwinContext context, string key) { return _innerManager.GetRequestCookie(context, key); } //private method to decide if we are dealing with old browsers private void CheckSameSite(IOwinContext context, CookieOptions options) { if (options.SameSite == Microsoft.Owin.SameSiteMode.None && DisallowsSameSiteNone(context.Request.Headers["User-Agent"])) { options.SameSite = null; } } }

 

The work is done in the AppendResponseCookie and DeleteCookie methods which append or delete (by setting cookies to an expiration date of the 1st of January 1970) cookies from responses having to do with requests for authentication. Typically AppendResponseCookie is called when authenticating the user, while DeleteCookie is called when logging off.

These methods call a method called CheckSameSite, which will look if the cookie options are already set to SameSite=None, and the Boolean flag obtained by calling the DisallowSameSiteNone is true or false. If both of the conditions evaluate to true, no SameSite attribute will be emitted, if not, no change is performed and the SameSite=None attribute is allowed on the cookie.

Finally, here is the code for the DisallowsSameSiteNone(string) method:

 

private bool DisallowsSameSiteNone(string userAgent) { // check if the user agent is null or empty if (String.IsNullOrWhiteSpace(userAgent)) return false; // 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; } return false; }

 

Please note that the code for this method is the same as the one posted by the ASP.net team in their blog on the same issue which can be read here. I have just made minor changes to check for user agents that are null, since this can occur with crawlers and bots that visit Internet facing websites.


I hope this article series helps you future proof your ASP.net sites to comply with the latest SameSite cookie recommendations.


written by: Paul Cociuba

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.