Sustainsys / Saml2

Saml2 Authentication services for ASP.NET
Other
951 stars 601 forks source link

Error No cookie preserving state from the request was found so the message was not expected to have an InResponseTo attribute. This error typically occurs if the cookie set when doing SP-initiated sign on have been lost. #1322

Closed taigon1984 closed 2 years ago

taigon1984 commented 2 years ago

I am running dotnet core 5, using Sustainsys.Saml2 & Sustainsys.Saml2.AspNetCore2 libraries version 2.8.0, using Visual Studio 2022.

I do NOT experience this issue locally with a DEBUGGER attached and every attempt to replicate the error while a debugger is attached fails. Everything works as expected.

I have 2 applications, an API, and a front end web project. I am using cookie authentication and sharing the cookies between the applications. The SignIn for Saml2 is initiated on the API application, and for authentication on the portal side I am using cookie authentication, and everything is working fine.

I can sign in no problems for the FIRST TIME ONLY with no debugger attached, I can sign out no problem, but when I try to navigate back after signing out by just clicking the back button, I am redirected to login to ADFS again to sign in, but after signing in the SECOND time, I receive this error message right after being redirected back to portalsite.com/Saml2/Acs/. I then have to close the browser and re-open it to sign in again.

I can tell you that in FireFox, this error does not appear whatsoever, regardless if I'm debugging or not, so I have a feeling it's the Chrome SameSite issue, but I can't figure out how to resolve it even from the suggestions provided throughout numerous forums.

I've tried for days now to identify what the issue is and have tried different cookie settings, workarounds, including ones described for this issue, but they do not seem to work.

Please advise what other type of details would help resolve this....

        app.UseCookiePolicy(new CookiePolicyOptions()
        {
            HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
            MinimumSameSitePolicy = SameSiteMode.None,
            Secure = CookieSecurePolicy.Always,
            OnAppendCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions),
            OnDeleteCookie = cookieContext => SameSite.CheckSameSite(cookieContext.Context, cookieContext.CookieOptions)
        });
taigon1984 commented 2 years ago

Here is how I setup the cookie and saml2 auth on the API side,

services.AddAuthentication("Identity.Application") .AddCookie("Identity.Application", opts => { opts.Cookie.SecurePolicy = CookieSecurePolicy.Always; opts.Cookie.SameSite = SameSiteMode.None; opts.Cookie.HttpOnly = false; opts.Cookie.Path = "/"; opts.ExpireTimeSpan = userExpiry; opts.SlidingExpiration = true; opts.LoginPath = "/account/signin"; opts.LogoutPath = "/account/signout"; opts.Cookie.Name = ".AspNet.SharedCookie"; opts.ExpireTimeSpan = TimeSpan.FromMinutes(60); opts.Cookie.IsEssential = true;

            opts.Events = new CookieAuthenticationEvents()
            {
                OnValidatePrincipal = async context =>
                {
                    if (context == null) throw new ArgumentNullException(nameof(context));

                    if (context.Principal == null)
                    {
                        context.RejectPrincipal();
                        return;
                    }
                }              
            };
        })
        .AddSaml2(opts =>
        {
            var config = BaseConfiguration.GetConfiguration();

            opts.SPOptions.ServiceCertificates.Add(GenericHelpers.FindCertificateFromStore(config.Saml2["SigningCertificateSubject"]));
            opts.ClaimsIssuer = config.Saml2["Issuer"];

            opts.SPOptions.EntityId = new EntityId(opts.ClaimsIssuer);
            opts.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
            opts.SPOptions.ReturnUrl = new Uri(config.Saml2["SamlLoginCallback"]);
            opts.SPOptions.OutboundSigningAlgorithm = config.Saml2["SignatureAlgorithm"];

            opts.IdentityProviders.Add(new IdentityProvider(new EntityId(config.Saml2["EntityId"]), opts.SPOptions)
            {
                MetadataLocation = config.Saml2["IdPMetadata"],
                RelayStateUsedAsReturnUrl = true,
                Binding = Saml2BindingType.HttpPost,
                LoadMetadata = true,
                SingleLogoutServiceUrl = new Uri(config.Saml2["SignOutUrl"]),
                SingleLogoutServiceBinding = Saml2BindingType.HttpPost
            });

            opts.Notifications.AcsCommandResultCreated = (commandResult, response) =>
            {
              //Just testing invocation
            };
        })
taigon1984 commented 2 years ago

This is my authentication on the web project side

        services.AddAuthentication("Identity.Application")
        .AddCookie("Identity.Application", opts =>
        {
            opts.Cookie.Name = ".AspNet.SharedCookie";
            opts.Cookie.Domain = ".lifemark.ca";

            opts.Events = new CookieAuthenticationEvents()
            {
                OnRedirectToLogin = (context) =>
                {
                    var apiSettings = BaseConfiguration.GetConfiguration().APISettings;
                    var path = context.HttpContext.Request.Path.Value;
                    var returnPath = System.Web.HttpUtility.UrlEncode($"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host.Value}{(path.StartsWith("/Account") ? "/" : path)}");
                    var signInCompleteReturnPath = System.Web.HttpUtility.UrlEncode($"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host.Value}/Account/SignInComplete?returnPath={returnPath}");

                    foreach (var cookie in context.HttpContext.Request.Cookies)
                        context.HttpContext.Response.Cookies.Append(cookie.Key, cookie.Value, new CookieOptions() { Domain = ".lifemark.ca", Path = "/" });

                    context.HttpContext.Response.Redirect($"{apiSettings.Address}/v{apiSettings.Version}/account/signin?returnPath={signInCompleteReturnPath}");
                    return Task.CompletedTask;
                },
                OnValidatePrincipal = async context =>
                {
                    if (context == null) throw new ArgumentNullException(nameof(context));

                    if (context.Principal == null)
                    {
                        context.RejectPrincipal();
                        return;
                    }

                    var user = context.Principal;

                    if (!await AuthHandler.Authorize(ref user, ServiceProvider.GetService<IAuthorizationService>(), context.HttpContext)
                    && !context.HttpContext.Request.Headers["Authorization"].Any())
                        context.Response.Redirect($"/Account/AccessDenied?ReturnUrl={(context.Request.Path.Value.StartsWith("/Acccount") ? "/" : context.Request.Path.Value)}");
                },
                OnSigningOut = async context =>
                {
                    foreach (var cookie in context.HttpContext.Request.Cookies)
                        context.HttpContext.Response.Cookies.Append(cookie.Key, cookie.Value, new CookieOptions() { Domain = "appsapilocal.lifemark.ca", Path = "/" });

                    var apiSettings = BaseConfiguration.GetConfiguration().APISettings;
                    context.HttpContext.Response.Redirect($"{apiSettings.Address}/v{apiSettings.Version}/account/signout");                        
                }
            };
        });
taigon1984 commented 2 years ago

Disregard this, I just found my answers in other previous topics. Just investigating these and will re-open later if needed.