Sustainsys.Saml2 - Saml2/Acs endpoint returns Error 500 when processing SSO

2.3k views Asked by At

I've created a C# ASP .Net Core 6.0 application, and trying to implement SSO with Azure AD using Sustainsys.Saml2, specifically with the Sustainsys.Saml2.AspNetCore2 package. Having tested the implementation on my development machine with localhost, I can see it works as expected and authenticates the user, populates the Identity model, and redirects to correct URL.

However, when deployed into the test environment, using a dockerized version, the behaviour changes. When triggering SSO, the user is authenticated successfully in Azure, but when returning to the app, it returns an Error 500 at the Saml2/Acs endpoint. Reviewing the logs show no indication of any errors, and instead report successful authentication for the user.

The Program.cs configuration:

 builder.Services.AddAuthentication(sharedOptions =>
    {
        sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        sharedOptions.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        sharedOptions.DefaultChallengeScheme = "Saml2";
    })
    .AddSaml2(options =>
    {
        var logger = new LoggerFactory();
        options.SPOptions.Logger = new AspNetCoreLoggerAdapter(logger.CreateLogger<Saml2Handler>());

        options.SPOptions.EntityId = new EntityId(AppConfig.Saml_EntityID);
        options.IdentityProviders.Add(
            new IdentityProvider(new EntityId(AppConfig.Saml_AzureID), options.SPOptions)
            {
                Binding = Saml2BindingType.HttpRedirect,
                LoadMetadata = true,
                MetadataLocation = AppConfig.Saml_Metadata,
                DisableOutboundLogoutRequests = false,
                AllowUnsolicitedAuthnResponse = true                
            });
    
        options.SPOptions.PublicOrigin = new Uri(AppConfig.BaseUrl);
        options.SPOptions.ReturnUrl = new Uri(AppConfig.BaseUrl);

        options.SPOptions.WantAssertionsSigned = true;
        options.SPOptions.AuthenticateRequestSigningBehavior = SigningBehavior.Always;
        options.SPOptions.ServiceCertificates.Add(new X509Certificate2(AppConfig.Saml_Cert_Path));
    })
    .AddCookie();

While troubleshooting the issue, I stumbled across some confusing behaviour that may or may not indicate what may be the issue. If I follow the following steps, I can end up at a point where the user is authenticated and can use the applications:

  1. Click 'Login' to trigger the Saml authentication.
  2. Hit the Error 500 at Saml2/Acs.
  3. Click 'refresh', and 'continue' to resubmit the request.
  4. The browser then continues to the intended URL, but says 'Connection Refused'
  5. Use the browser back buttons to return to the application home screen, and refresh the page... Viola! Logged in!

Furthermore, when inspecting the request headers on the Saml2/Acs endpoint, I can see a Saml response is returned, which I can manually decode from base64 and read the correct information!

As mentioned, the logs don't mention any errors, just:

Initiating login to https://sts.windows.net/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/

and

Successfully processed SAML response _ba082bb8-7d2c-4aa4-a7dc-b1520312d084 and authenticated a*******@********.com

Any assistance, or guidance to a resolution would be much appreciated!

1

There are 1 answers

0
user2681502 On

Maybe this is not relevant for .Net Core, but for .net framework 4.8 there was the following issues:

  1. ReturnUrl of Service Provider was wrong: http://locahost/mysite/saml2/acs instead of correct one http://locahost/mysite/ (with trailing slash). Because of this, there was indefinite loop to http://locahost/mysite/saml2/acs`.

    SPOptions spOptions = new SPOptions() { EntityId = new EntityId(spMetadataUrl), ReturnUrl = new Uri(hostUrl + "/"), DiscoveryServiceUrl = new Uri(hostUrl + @"/DiscoveryService"), Organization = organization, AuthenticateRequestSigningBehavior = SigningBehavior.Never, RequestedAuthnContext = requestedAuthnContext, Logger = logger, PublicOrigin = hostUri };

  2. DO NOT USE UseExternalSignInCookie mehod, otherwise ClaimPrincipal will not set for current Thread (cookies will be parsed to claims, although latter will not be set, this can be checked with code below):

Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate); options.SPOptions.Saml2PSecurityTokenHandler = new MySaml2PSecurityTokenHandler();

     public class MySaml2PSecurityTokenHandler : Sustainsys.Saml2.Saml2P.Saml2PSecurityTokenHandler
        {
            protected override ClaimsIdentity CreateClaimsIdentity(Saml2SecurityToken samlToken, string issuer, TokenValidationParameters validationParameters)
            {
                ClaimsIdentity identity = base.CreateClaimsIdentity(samlToken, issuer, validationParameters);
                Claim claim = new Claim("Name", "jon.doe");
                Claim[] claims = new Claim[] { claim };
                identity.AddClaims(claims);
    
                return identity;
            }
    }

Additionally only for Net Framework 4.8, because of owin vs System.Web cookies bug, CookieManager should be used. Code:

var cookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebCookieManager();    

            Saml2AuthenticationOptions options = CreateSaml2Options(configuration, certificate);

            CookieAuthenticationOptions cookieAuthentication = new CookieAuthenticationOptions()
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString(configuration.ServiceProviderSignOnUrl),
                CookieManager = сookieManager,
                ReturnUrlParameter = GetBase() + "/",

                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 = (context) =>
                        {
                            var newIdentity = new ClaimsIdentity(context.Identity);
                            int newcount = 1;
                            newIdentity.AddClaim(new Claim("SIMPLECOUNT", newcount.ToString()));
                            context.ReplaceIdentity(newIdentity);

                            return Task.FromResult<object>(null);
                        }
                }
            };

            app.UseCookieAuthentication(cookieAuthentication);
            app.SetDefaultSignInAsAuthenticationType(cookieAuthentication.AuthenticationType);

            //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
            app.UseSaml2Authentication(options);