AspNetKatana: Aspnet External Cookie not set for Sign On with Microsoft Post 4.72 SameSite Changes on Asp.net MVC

Apologies for being a bit verbose, but want to try and be specific. I am having trouble with .AspNet.ExternalCookie after the same site changes of Nov 2019 and Azure update of Jan 2020

Setup:

  • .net Framework 4.72 Post samesite changes
  • Azure/IIS on windows 10
  • asp.net MVC

Issue: Sign on with Microsoft was broken with the changes to support SameSite on Azure. We were able to bandaid fix by applying the “roll back” technique of aspnet:SuppressSameSiteNone.

Symptoms: With the suppress same site flag post logging in with Microsoft the .AspNet.ExternalCookie is set but without samesite:none. Without the flag the cookie is never set (or at least in a way I can see it)

What I have tried

this sounds like the problem on https://github.com/aspnet/AspNetKatana/issues/324 which was closed, without a real resolution - the requester said when he re-installed the the path the bug returned.

my code, apology for a lot of it, I tried to rip out as much as I could that was extraneous.

using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin;
using Microsoft.Owin.Cors;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Infrastructure;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OAuth;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Abc.Auth.Identity.DBContext;
using Abc.Auth.Identity.Helpers;
using Abc.Auth.Identity.Models;
using Abc.Auth.Providers;
using Abc.Constants.AppSettings;
using System;
using System.Threading.Tasks;

namespace Abc.Web
{
    public class Startup
    {
        public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }


        //Copied from https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Security.Cookies/Provider/DefaultBehavior.cs
        private static bool IsAjaxRequest(IOwinRequest request)
        {
            IReadableStringCollection query = request.Query;
            if (query != null)
            {
                if (query["X-Requested-With"] == "XMLHttpRequest")
                {
                    return true;
                }
            }

            IHeaderDictionary headers = request.Headers;
            if (headers != null)
            {
                if (headers["X-Requested-With"] == "XMLHttpRequest")
                {
                    return true;
                }
            }
            return false;
        }


        public virtual void Configuration(IAppBuilder app)
        {

            ConfigureAuth(app);
        }
        public void ConfigureAuth(IAppBuilder app)
        {

            // Configure the db context, user manager and role manager to use a single instance per request
            app.CreatePerOwinContext(MyIdentityDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create);

            // 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

            var cao = new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/account/logon"),
                LogoutPath = new PathString("/account/logout"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser, int>(
                       validateInterval: TimeSpan.FromMinutes(20),
                       regenerateIdentityCallback: (manager, user) => user.GenerateUserIdentityAsync(manager, DefaultAuthenticationTypes.ApplicationCookie),
                       getUserIdCallback: (id) => (Int32.Parse(id.GetUserId()))),

                    // Same logic as https://github.com/aspnet/AspNetKatana/blob/dev/src/Microsoft.Owin.Security.Cookies/Provider/DefaultBehavior.cs
                    // from the "internal static readonly Action<CookieApplyRedirectContext> ApplyRedirect" method, except if its an ajax request we just return the code 
                    // instead of the "X-Responded-JSON with 200 response" nonsense
                    OnApplyRedirect = (ctx =>
                    {
                        if (!IsAjaxRequest(ctx.Request))
                        {
                            ctx.Response.Redirect(ctx.RedirectUri);
                        }
                    }),

                    /* I changed this part */
                    OnException = (context =>
                    {
                        throw context.Exception;
                    })
                },

                // Potential fix for OWIN authentication issues:
                //http://katanaproject.codeplex.com/wikipage?title=System.Web%20response%20cookie%20integration%20issues&referringTitle=Documentation
                //https://katanaproject.codeplex.com/workitem/197
                //https://stackoverflow.com/questions/20737578/asp-net-sessionid-owin-cookies-do-not-send-to-browser
                CookieManager = new SameSiteCookieManager(new SystemWebCookieManager())

            };

            app.UseCookieAuthentication(cao);
            app.UseExternalSignInCookie();

            Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions office365 = CreateOffice365Options();

            app.UseOpenIdConnectAuthentication(office365);

        }

        private OpenIdConnectAuthenticationOptions CreateOffice365Options()
        {
            var office365 = new Microsoft.Owin.Security.OpenIdConnect.OpenIdConnectAuthenticationOptions();
            office365.ClientId = Statics.Office365ClientId; ;
            office365.Caption = "Office 365";

            office365.Authority = Statics.Office365AuthorityBaseUri;

            office365.CookieManager = new SameSiteCookieManager(new SystemWebCookieManager());

            office365.TokenValidationParameters = new TokenValidationParameters
            {
                // instead of using the default validation (validating against a single issuer value, as we do in line of business apps (single tenant apps)),
                // we turn off validation
                //
                // NOTE:
                // * In a multitenant scenario you can never validate against a fixed issuer string, as every tenant will send a different one.
                // * If you don’t care about validating tenants, as is the case for apps giving access to 1st party resources, you just turn off validation.
                // * If you do care about validating tenants, think of the case in which your app sells access to premium content and you want to limit access only to the tenant that paid a fee,
                //       you still need to turn off the default validation but you do need to add logic that compares the incoming issuer to a list of tenants that paid you,
                //       and block access if that’s not the case.
                // * Refer to the following sample for a custom validation logic: https://github.com/AzureADSamples/WebApp-WebAPI-MultiTenant-OpenIdConnect-DotNet

                ValidateIssuer = false
            };
            office365.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.

                RedirectToIdentityProvider = (context) =>
                {
                    // This ensures that the address used for sign in and sign out is picked up dynamically from the request
                    // this allows you to deploy your app (to Azure Web Sites, for example)without having to change settings
                    // Remember that the base URL of the address used here must be provisioned in Azure AD beforehand.
                    string appBaseUrl = context.Request.Scheme + Uri.SchemeDelimiter + context.Request.Host + context.Request.PathBase;
                    context.ProtocolMessage.RedirectUri = appBaseUrl + "/account/office365";
                    context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;

                    return Task.FromResult(0);
                },

                AuthenticationFailed = (context) =>
                {
                    // Suppress the exception if you don't want to see the error

                    //Elmah.ErrorSignal.FromCurrentContext().Raise(new Exception($"DEBUG: O365 auth failed: {context.Exception.Message}"));
                    Elmah.ErrorSignal.FromCurrentContext().Raise(context.Exception);
                    context.HandleResponse();
                    return Task.FromResult(0);
                }
            };

            return office365;
        }

    }



    public class SameSiteCookieManager : ICookieManager
    {
        private readonly ICookieManager _innerManager;

        public SameSiteCookieManager() : this(new CookieManager())
        {
        }

        public SameSiteCookieManager(ICookieManager innerManager)
        {
            _innerManager = innerManager;
        }

        public void AppendResponseCookie(IOwinContext context, string key, string value,
                                         CookieOptions options)
        {
            CheckSameSite(context, options);
            _innerManager.AppendResponseCookie(context, key, value, options);
        }

        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 void CheckSameSite(IOwinContext context, CookieOptions options)
        {
            if (options.SameSite == SameSiteMode.None && DisallowsSameSiteNone(context))
            {
                options.SameSite = null;
            }
        }

        public static bool DisallowsSameSiteNone(IOwinContext context)
        {
            return false;
        }
    }

}

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 24 (12 by maintainers)

Most upvoted comments

Does the LinkedInAuthenticationOptions allow changing CookieManager? I can’t see that option in LinkedInAuthenticationOptions

That’s not one we maintain. Looks like it came from here? It hasn’t been updated in a while, someone might have to go add that option.