Sustainsys / Saml2

Saml2 Authentication services for ASP.NET
Other
940 stars 606 forks source link

Federated logout not possible, redirecting to post-logout #1456

Closed kevinmcody closed 2 weeks ago

kevinmcody commented 4 weeks ago

Sustainsys.Saml2.AspNetCore2 v 2.9.1

I started from the DuendeDynamicProviders sample found here:
https://github.com/Sustainsys/Saml2.Sample

I am trying to get single signout working. After calling the Signout method in the Logout page, like so:

return SignOut(new AuthenticationProperties { RedirectUri = url }, idp);

I see the following in the log:

[16:32:39 Information] Sustainsys.Saml2.AspNetCore2.Saml2Handler Federated logout not possible, redirecting to post-logout

I did some googling, and most solutions involved making sure the following claims were carried over from the external user to the local:

  1. http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
  2. http://Sustainsys.se/Saml2/LogoutNameIdentifier
  3. http://Sustainsys.se/Saml2/SessionIndex

I accomplished this by modifying the CaptureExternalLoginContext method in the ExterlanLogin/Callback page, like so:

var nameIdentifierClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
if (nameIdentifierClaim != null)
{
    localClaims.Add(new Claim(ClaimTypes.NameIdentifier, nameIdentifierClaim.Value));
}

var logoutNameIdentifierClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == "http://Sustainsys.se/Saml2/LogoutNameIdentifier");
if (logoutNameIdentifierClaim != null)
{
    localClaims.Add(new Claim("http://Sustainsys.se/Saml2/LogoutNameIdentifier", logoutNameIdentifierClaim.Value));
}

var sessionIndexClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == "http://Sustainsys.se/Saml2/SessionIndex");
if (sessionIndexClaim != null)
{
    localClaims.Add(new Claim("http://Sustainsys.se/Saml2/SessionIndex", sessionIndexClaim.Value));
}

I also saw some people claiming you had to explicitly set certain fields on the Sustainsys.Saml2.IdentityProvider, so I modified DynamicProviderUtils.Saml2ConfigureOptions.cs line 47 like so:

if(options.IdentityProviders.IsEmpty)
{
   var newIdp = 
        new Sustainsys.Saml2.IdentityProvider(
            new EntityId(idp.IdpEntityId), options.SPOptions)
        {
            LoadMetadata = true,
            Binding = Saml2BindingType.HttpPost,
            SingleSignOnServiceUrl = new Uri("https://stubidp.sustainsys.com/"),
            SingleLogoutServiceBinding = Saml2BindingType.HttpRedirect,
            SingleLogoutServiceUrl = new Uri("https://stubidp.sustainsys.com/Logout"),
            AllowUnsolicitedAuthnResponse = true,
            DisableOutboundLogoutRequests = false,
        };
    newIdp.SigningKeys.AddConfiguredKey(new X509Certificate2("Sustainsys.Saml2.Tests.pfx"));
    options.IdentityProviders.Add(newIdp);
}

I'm very new to identity management in general. Any ideas what to try next?

AndersAbel commented 3 weeks ago

There are quite a few things required for logout to work. One (unexpected) thing is that there must be a service certificate configure on the SP.

The full check is logged if you switch the log level up to Verbose: https://github.com/Sustainsys/Saml2/blob/594e3b311734c26c7bd87e8f78c176507c340ef7/Sustainsys.Saml2/WebSSO/LogOutCommand.cs#L174-L180

kevinmcody commented 2 weeks ago

Thanks for that! You were right - it was the Signing Certificate. I fixed this by adding the following to DynamicProviderUtils.Saml2ConfigureOptions.cs line 42:

options.SPOptions.ServiceCertificates.Add(new X509Certificate2("Sustainsys.Saml2.Tests.pfx"));

Also, my previous modification of capturing the additional claims was not quite correct. I was re-creating the claims, thus changing the issuer, which the logout checks also did not like. CaptureExternalLoginContext method in the ExterlanLogin/Callback page now looks like:

// if the external login is OIDC-based, there are certain things we need to preserve to make logout work
// this will be different for WS-Fed, SAML2p or other protocols
private void CaptureExternalLoginContext(AuthenticateResult externalResult, List<Claim> localClaims, AuthenticationProperties localSignInProps)
{
    // if the external system sent a session id claim, copy it over
    // so we can use it for single sign-out
    var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
    if (sid != null)
    {
        localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
    }

    var nameIdentifierClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == ClaimTypes.NameIdentifier);
    if (nameIdentifierClaim != null)
    {
        localClaims.Add(nameIdentifierClaim);
    }

    var logoutNameIdentifierClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == "http://Sustainsys.se/Saml2/LogoutNameIdentifier");
    if (logoutNameIdentifierClaim != null)
    {
        localClaims.Add(logoutNameIdentifierClaim);
    }

    var sessionIndexClaim = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == "http://Sustainsys.se/Saml2/SessionIndex");
    if (sessionIndexClaim != null)
    {
        localClaims.Add(sessionIndexClaim);
    }

    // if the external provider issued an id_token, we'll keep it for signout
    var idToken = externalResult.Properties.GetTokenValue("id_token");
    if (idToken != null)
    {
        localSignInProps.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
    }
}

Side note: I did NOT have to manually set all the properties on the IdentityProvider. Using LoadMetadata=true caused them to be read correctly from the metadata xml.