The SAML response signature failed to verify when multiple Idp configurations present

hello - I am using component space version 4.8.0 for .net core.

SAML signature verification fails when multiple SAML configurations are loaded, but works perfectly with a single configuration.

Multiple configurations: Attempt to first Idp fails with SamlSignatureException, attempt to 2nd Idp succeeds. Logs show: “XML signature verified: false” and i can see from trace that its trying to match the first response with an invalid cert.

Again both configurations individually work. ComponentSpace is using Certificate A (from configuration) to verify a SAML response that was signed with Certificate B (embedded in response). The certificates are from different Azure AD tenants.

We have made sure that when the response is received at the ACS endpoint, we explicitly set the correct configuration value through the SetConfigurationNameAsync() method.

SAML configuration code

private static void ConfigureSaml(
SamlConfigurations samlConfig,
IAuthService authService,
IcustomUrlService customUrlService,
IMetadataToConfiguration metadata,
IHttpContextAccessor httpContextAccessor)
{
var settingList = authService.GetAll()
.Where(x => x.FederationTypeId == (int)EnumUtility.FederationType.SAML2);
var httpRequest = httpContextAccessor.HttpContext.Request;

samlConfig.Configurations = new List<SamlConfiguration>();

foreach (var setting in settingList)
{
    SamlConfiguration idpConfiguration;
    var clientFedSettingDetail = authService.GetById(setting.settingId);

    if (clientFedSettingDetail.ClientDetailsettings.Count == 0)
        continue;

    foreach (var fedSetting in clientFedSettingDetail.ClientDetailsettings)
    {
        var clientId = fedSetting.ClientDetailId;

        var customUrl = customUrlService.GetClientIdentifier(clientId).GetAwaiter().GetResult();

        if (customUrl is null)
            continue;

        if (string.IsNullOrWhiteSpace(setting.MetadataAddress) && 
            string.IsNullOrWhiteSpace(setting.MetadataXml))
            throw new InvalidOperationException("SAML2 metadata is missing");

        // Load IdP configuration from metadata
        if (!string.IsNullOrWhiteSpace(setting.MetadataAddress))
        {
            // MetadataAddress examples:
            // https://login.microsoftonline.com/tenant1-guid/federationmetadata/2007-06/federationmetadata.xml?appid=app1-guid
            // https://login.microsoftonline.com/tenant2-guid/federationmetadata/2007-06/federationmetadata.xml?appid=app2-guid
            idpConfiguration = metadata.ImportAsync(setting.MetadataAddress).GetAwaiter().GetResult();
        }
        else
        {
            var xmlDocument = new XmlDocument();
            xmlDocument.LoadXml(setting.MetadataXml);
            idpConfiguration = metadata.Import(xmlDocument.DocumentElement);
        }

        if (idpConfiguration is null)
            throw new InvalidOperationException("Unable to fetch Idp Configuration from metadata");

        // Optional: Modify SSO URL with community document ID
        if (!string.IsNullOrWhiteSpace(setting.CommunityDocumentId))
        {
            var ssoUrl = idpConfiguration.PartnerIdentityProviderConfigurations.First().SingleSignOnServiceUrl;
            idpConfiguration.PartnerIdentityProviderConfigurations.First().SingleSignOnServiceUrl = 
                ssoUrl + "/" + setting.CommunityDocumentId;
        }

        // Configure signature settings
        idpConfiguration.PartnerIdentityProviderConfigurations.First().WantSamlResponseSigned = true;
        idpConfiguration.PartnerIdentityProviderConfigurations.First().SignAuthnRequest = false;

        // Build ACS URLs with optional URL identifier
        var baseUrl = $"{httpRequest.Scheme}://{httpRequest.Host.ToUriComponent()}/{customUrl.Url}";
        var urlWithIdentifier = !string.IsNullOrWhiteSpace(fedSetting.UrlIdentifier)
            ? $"{baseUrl}/{fedSetting.UrlIdentifier}"  // e.g., /client1/identifier1/Saml2Auth/AssertionConsumerService
            : baseUrl;                                // e.g., /client1/Saml2Auth/AssertionConsumerService

        var localSpConfiguration = new LocalServiceProviderConfiguration()
        {
            Name = setting.ValidIssuer,  // e.g., "https://myapp.com/tenant1" or "https://myapp.com/tenant2"
            AssertionConsumerServiceUrl = $"{urlWithIdentifier}/Saml2Auth/AssertionConsumerService",
            SingleLogoutServiceUrl = $"{urlWithIdentifier}/Saml2Auth/SingleLogoutService"
        };

        // Add configuration with complete isolation attempt
        samlConfig.Configurations.Add(new SamlConfiguration
        {
            Name = setting.AuthenticationScheme,  // e.g., "TENANT1_SAML" or "TENANT2_SAML"
            LocalServiceProviderConfiguration = new LocalServiceProviderConfiguration()
            {
                Name = setting.ValidIssuer,
                AssertionConsumerServiceUrl = urlWithIdentifier + "/Saml2Auth/AssertionConsumerService",
                SingleLogoutServiceUrl = urlWithIdentifier + "/Saml2Auth/SingleLogoutService"
            },
            // Clone partner configurations to avoid shared references
            PartnerIdentityProviderConfigurations = idpConfiguration.PartnerIdentityProviderConfigurations
                .Select(p => new PartnerIdentityProviderConfiguration
                {
                    Name = p.Name,
                    PartnerCertificates = p.PartnerCertificates,  // Different certificates per tenant
                    SingleSignOnServiceUrl = p.SingleSignOnServiceUrl,
                    WantSamlResponseSigned = p.WantSamlResponseSigned,
                    SignAuthnRequest = p.SignAuthnRequest
                })
                .ToList()
        });
    }
}

}

Code made for the SAML request:

await _samlServiceProvider.SetConfigurationNameAsync(configName);
await _samlServiceProvider.InitiateSsoAsync(relayState: returnUrl ?? Url.Content(“~/”));

Code at the ACS endpoint;

await _samlServiceProvider.SetConfigurationNameAsync(configName);
var ssoResult = await _samlServiceProvider.ReceiveSsoAsync();

Are you saying that the wrong SAML configuration is being used even though SetConfigurationNameAsync is being called specifying the correct configuration name?

InitiateSsoAsync and ReceiveSsoAsync will use the SAML configuration specified by SetConfigurationNameAsync. The PartnerIdentityProviderConfiguration within the selected SamlConfiguration is used to process the SAML response including XML signature verification.

If the issue only occurs with multiple SAML configurations, it suggests either the wrong configuration is being selected or there’s some issue setting up the multiple configurations.

Please double check the SAML configurations are correct and that the correct configuration is being selected.

If there’s still an issue, please enable SAML Trace and send the generated log file as an email attachment to support@componentsace.com mentioning your forum post.

thank you for the quick response, sent the logs over