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();