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?
NameId
in SignIn
action need to be something else ("abc" is really send as NameId
)validationMode
?"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")
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:
SHA256
.