Search code examples
c#asp.net-core-mvcsaml-2.0itfoxtec-identity-saml2

SSO and signature validation using ITfoxtec.Identity.Saml2.MvcCore 4.8.5 in .NET Core 6


I'm trying to implement single sign on using ITfoxtec.Identity.Saml2.MvcCore package. Request is successfully sent, user is redirected to identity provider's login screen and after entering correct username and password redirected back to service provider (my localhost aspnet app).

Unfortunately, I'm getting this exception:

InvalidSignatureException: Signature is invalid.

Here are relevant code parts (from ITfoxtec net core sample) and metadata. It's heavily redacted because of the client.

Metadata is as follows:

<md:EntityDescriptor
    xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata" entityID="https://xxxx" ID="xxxxx">
    <md:IDPSSODescriptor ID="xxxxx" protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" WantAuthnRequestsSigned="false">
        <md:KeyDescriptor>
            <KeyInfo
                xmlns="http://www.w3.org/2000/09/xmldsig#">
                <X509Data>
                    <X509Certificate>MIIxxxxxxxxABC</X509Certificate>
                </X509Data>
            </KeyInfo>
        </md:KeyDescriptor>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://xxxxxxx/idp/slo"/>
        <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://xxxxxxx/idp/slo"/>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:persistent</md:NameIDFormat>
        <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat>
        <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://xxxxxxxx/idp/sso"/>
        <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://xxxxxx/idp/sso"/>
    </md:IDPSSODescriptor>
</md:EntityDescriptor>

Configuration in startup is reading metadata and config:

builder.Services.BindConfig<Saml2Configuration>(builder.Configuration, "Saml2", (serviceProvider, saml2Configuration) =>
{
    //saml2Configuration.SignAuthnRequest = true;
    
    /////saml2Configuration.SigningCertificate = CertificateUtil.Load(builder.Environment.MapToPhysicalFilePath(builder.Configuration["Saml2:SigningCertificateFile"]), builder.Configuration["Saml2:SigningCertificatePassword"], X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);

    //Alternatively load the certificate by thumbprint from the machines Certificate Store.
    //saml2Configuration.SigningCertificate = CertificateUtil.Load(StoreName.My, StoreLocation.LocalMachine, X509FindType.FindByThumbprint, Configuration["Saml2:SigningCertificateThumbprint"]);

    //saml2Configuration.SignatureValidationCertificates.Add(CertificateUtil.Load(AppEnvironment.MapToPhysicalFilePath(Configuration["Saml2:SignatureValidationCertificateFile"])));
    saml2Configuration.AllowedAudienceUris.Add(saml2Configuration.Issuer);

    var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
    var entityDescriptor = new EntityDescriptor();
    entityDescriptor.ReadIdPSsoDescriptorFromUrlAsync(httpClientFactory, new Uri(builder.Configuration["Saml2:IdPMetadata"])).GetAwaiter().GetResult();
    if (entityDescriptor.IdPSsoDescriptor != null)
    {
        saml2Configuration.AllowedIssuer = entityDescriptor.EntityId;
        saml2Configuration.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
        saml2Configuration.SingleLogoutDestination = entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.First().Location;
        foreach (var signingCertificate in entityDescriptor.IdPSsoDescriptor.SigningCertificates)
        {
            if (signingCertificate.IsValidLocalTime())
            {
                saml2Configuration.SignatureValidationCertificates.Add(signingCertificate);
            }
        }
        if (saml2Configuration.SignatureValidationCertificates.Count <= 0)
        {
            throw new Exception("The IdP signing certificates has expired.");
        }
        if (entityDescriptor.IdPSsoDescriptor.WantAuthnRequestsSigned.HasValue)
        {
            saml2Configuration.SignAuthnRequest = entityDescriptor.IdPSsoDescriptor.WantAuthnRequestsSigned.Value;
        }
    }
    else
    {
        throw new Exception("IdPSsoDescriptor not loaded from metadata.");
    }

    return saml2Configuration;
});

builder.Services.AddSaml2(slidingExpiration: true);

Config:

  "Saml2": {
    "IdPMetadata": "https://xxxxxx/idp/metadata?signAlgorithm=SHA1",
    "Issuer": "https://localhost:7021",
    //////"SingleSignOnDestination": "https://test-adfs.itfoxtec.com/adfs/ls/",
    //////"SingleLogoutDestination": "https://test-adfs.itfoxtec.com/adfs/ls/",
    "SignatureAlgorithm": "http://www.w3.org/2001/04/xmldsig-more#rsa-sha1",
    "SigningCertificateFile": "cert.pfx",
    "SigningCertificatePassword": "pass123",
    //"SignatureValidationCertificateFile": "cert.crt",
    "CertificateValidationMode": "None", // "ChainTrust"
    "RevocationMode": "NoCheck"

And this is the code for sign in:

public IActionResult SignIn(string? returnUrl= null)
{
    var binding = new Saml2RedirectBinding();
    binding.SetRelayStateQuery(new Dictionary<string, string> { { relayStateReturnUrl, returnUrl ?? Url.Content("~/") } });

    return binding.Bind(new Saml2AuthnRequest(_config)
        {
            //ForceAuthn = true,
            Subject = new Subject { NameID = new NameID { ID = "abcd" } },
            NameIdPolicy = new NameIdPolicy { AllowCreate = true, Format = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent" },
            //Extensions = new AppExtensions(),
            //RequestedAuthnContext = new RequestedAuthnContext
            //{
            //    Comparison = AuthnContextComparisonTypes.Exact,
            //    AuthnContextClassRef = new string[] { AuthnContextClassTypes.PasswordProtectedTransport.OriginalString },
            //},
        }).ToActionResult();

}

Code for assertion:

[HttpPost("/sso")]
public async Task<IActionResult> Sso()
{
    var binding = new Saml2PostBinding();
    var saml2AuthnResponse = new Saml2AuthnResponse(_config);

    binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);

    if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
    {
        throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
    }

    binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);

    await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));

    var relayStateQuery = binding.GetRelayStateQuery();
    var returnUrl = relayStateQuery.ContainsKey(relayStateReturnUrl) ? relayStateQuery[relayStateReturnUrl] : Url.Content("~/");

    return Redirect(returnUrl);
}

Response status (saml2AuthnResponse.Status) is Saml2StatusCodes.Success but on bind.Unbind exception is thrown.

So, what I'm doing wrong?

  • Does commented out lines related to certificates in configuration need to be uncommented?
  • I've generated SHA256 self signed certificate and provided public key to IdP. Does it have to be SHA1? (metadata has ?signAlgorithm=SHA1 parameter)
  • does NameId in SignIn action need to be something else ("abc" is really send as NameId)
  • Maybe some different validationMode?
  • Is it because of "SignatureAlgorithm? Is that signature algorithm for Request and certificate specified in metadata or for my certificate(s)?

I'm running in circles, so I'll be grateful for any answer that will point me to right direction.

Thank you

EDIT: when validating response on SAMLTools page, using my public key (certificate I've sent previously to IdP) and password from config I'm also getting "signature validation failed")


Solution

  • I think you need to debug one step at the time to find the problem.

    If not set in configuration SignatureAlgorithm defaults to SHA256. You should use SHA256 if possible.

    Thinks to try:

    1. Use the provided sample certificate (password: !QAZ2wsx) in your IdP, and configure it to use SHA256.
    2. Get the sample IdP and a sample RP to run together performing both login and logout.
    3. Then change the sample IdP to use your certificate.
    4. And thereafter change the sample RP to use your IdP.