We have been using the ComponentSpace.Saml2 for ASP.NET Core component for a few years for SAML SSO, with our company as the SP and our external partners as the IDPs. We have another SAML SSO flow where we are the IDP and an external partner is the SP, and we’re attempting to upgrade it from UltimateSAML to ComponentSpace.Saml2. I followed the code examples in the ExamplesVS2022 solution > ExampleIdentityProvider project (see code below) but I’m running into an issue.
The _samlIdentityProvider.ReceiveSsoAsync call is working great for receiving the SAMLRequest from the SP, however after verifying the user is signed in and calling _samlIdentityProvider.SendSsoAsync, the SAMLResponse is not signed so the SP throws an error. I set breakpoints and debugged the SamlConfigResolver to ensure that it was being called and setting the IDP’s LocalCertificates in the GetLocalIdentityProviderConfigurationAsync method and the SP’s PartnerCertificates in the GetPartnerServiceProviderConfigurationAsync method, and it’s worth noting that this same SamlConfigResolver is working great for the opposite methods when our company is the SP. So what am I missing for getting this SAML Response signed?
Here is the SAML response: https://gist.github.com/justintoth/521e91a55fc222dcf77854cc26644bb2
…and here is the relevant code:
SamlConfigResolver: (abbreviated)
public override Task<bool> IsLocalIdentityProviderAsync(string configurationName) => Task.FromResult(true);
public override Task GetLocalIdentityProviderConfigurationAsync(string configurationName)
{
var config = new LocalIdentityProviderConfiguration()
{
Name = “<a href=“https://www.ourcompany.com””>https://www.ourcompany.com",
Description = “Our Company Identity Provider”,
SingleSignOnServiceUrl = $“{AppSettings.AuthServerUrl}/sso/idp/initiate”,
LocalCertificates = new List
{
new Certificate
{
StoreName = ConfigurationManager.GetAppSetting(“CertificateStoreName”),
StoreLocation = ConfigurationManager.GetAppSetting(“CertificateStoreLocation”),
Thumbprint = ConfigurationManager.GetAppSetting(“CertificateThumbprintAuthSamlSigning”).ToLowerInvariant()
}
}
};
return Task.FromResult(config);
}
public override Task GetPartnerServiceProviderConfigurationAsync(string configurationName, string partnerName)
{
var serviceProvider = GetServiceProviders().FirstOrDefault(sp => sp.RequestIssuer == partnerName);
var config = new PartnerServiceProviderConfiguration
{
Name = serviceProvider.RequestIssuer,
Description = serviceProvider.SsoCode,
AssertionConsumerServiceUrl = serviceProvider.AuthenticationResponseUrl,
PartnerCertificates = new List
{
new Certificate
{
String = Convert.ToBase64String(serviceProvider.CertificateBytes)
}
}
};
return Task.FromResult(config);
}
public override Task<IList<string>> GetPartnerServiceProviderNamesAsync(string configurationName)
{
var serviceProviders = GetServiceProviders();
IList<string> partnerServiceProviderNames = serviceProviders
.Select(idp => idp.RequestIssuer)
.ToList();
return Task.FromResult(partnerServiceProviderNames);
}
SsoIdpController: (abbreviated)
[HttpGet(“initiate”)]
public async Task InitiateAsync()
{
ServiceProvider serviceProvider = null;
try
{
// SP-initiated SSO… Receive the authn request (as configured in SamlConfigResolver) from the SP.
var result = await _samlIdentityProvider.ReceiveSsoAsync();
// Look up the SP from the request issuer in the authn request.
serviceProvider = SingleSignOn.Model.Instance.GetServiceProviderByRequestIssuer(result.PartnerName);
if (!RequestIsValid(result.PartnerName, serviceProvider, out var error))
throw new Exception(error);
var isAuthenticated = HttpContext.User?.Identity.IsAuthenticated ?? false;
if (isAuthenticated)
{
// User is signed in, complete SSO immediately.
await CompleteSsoAsync(serviceProvider.SsoCode, auditEntryId);
return new EmptyResult();
}
else
{
// Otherwise, have user sign in first then complete SSO.
var returnUrl = $“{AppSettings.AuthServerUrl}/sso/idp/complete?ssoCode={serviceProvider.SsoCode}&auditEntryId={auditEntryId}”;
return RedirectToSignin(returnUrl);
}
}
catch (Exception exc)
{
LogError(auditEntryId, serviceProvider?.SsoCode, false, $“Initiate failed, error: {exc.Message}”, exc);
return RedirectToSignin();
}
}
[Authorize]
[HttpGet(“complete”)]
public async Task CompleteAsync(string ssoCode, long auditEntryId)
{
try
{
await CompleteSsoAsync(ssoCode, auditEntryId);
return new EmptyResult();
}
catch (Exception exc)
{
LogError(auditEntryId, ssoCode, true, $“Complete failed, error: {exc.Message}”, exc);
return new ForbidResult();
}
}
private static bool RequestIsValid(string requestIssuer, ServiceProvider serviceProvider, out string error)
{
error = “”;
// Validate that SSO is enabled.
if (!SingleSignOn.Model.Instance.IsSsoEnabled)
{
error = “SSO is not enabled”;
return false;
}
if (!SingleSignOn.Model.Instance.IsSsoIdentityProviderEnabled)
{
error = “Our Company as a SSO identity provider is not enabled”;
return false;
}
// Validate SP.
if (serviceProvider == null)
{
error = $“Could not find service provider with request issuer: {requestIssuer}”;
return false;
}
if (serviceProvider.IsDisabled)
{
error = $“Service provider with ssoCode: {serviceProvider.SsoCode} is disabled”;
return false;
}
return true;
}
private ActionResult RedirectToSignin(string returnUrl = null)
{
var signinUrl = $“{AppSettings.AuthServerUrl}/auth/sign-in”;
return Redirect(String.IsNullOrEmpty(returnUrl) ? signinUrl : $“{signinUrl}?ReturnUrl={returnUrl}”);
}
private Task CompleteSsoAsync(string ssoCode, long auditEntryId)
{
// Get user data.
var user = HttpContext.User;
var userName = user?.Identity.Name;
// Respond to the authn request by sending a SAML response containing a SAML assertion.
var emailAddress = user?.FindFirst(“email”)?.Value;
var firstName = user?.FindFirst(“given_name”)?.Value;
var lastName = user?.FindFirst(“family_name”)?.Value;
var attributes = new List
{
new SamlAttribute(“LoginEmailAddress”, emailAddress),
new SamlAttribute(“FirstName”, firstName),
new SamlAttribute(“LastName”, lastName),
};
return _samlIdentityProvider.SendSsoAsync(userName, attributes);
}