.NET Core 6中ITfoxtec.Saml2.MvcCore实现SSO签名验证失败
ITfoxtec.Identity.Saml2.MvcCore SSO签名验证失败问题排查
问题概述
使用ITfoxtec.Identity.Saml2.MvcCore实现单点登录,请求发送成功并跳转至IdP登录页面,输入凭证后返回SP时抛出InvalidSignatureException: Signature is invalid,SAML响应状态为Saml2StatusCodes.Success,但binding.Unbind执行失败。
相关代码与元数据
IdP元数据
<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>
Startup配置
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);
配置文件
"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" }
登录代码
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(); }
断言处理代码
[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); }
疑问解答
注释的证书代码是否需要取消注释?
- 不需要。IdP元数据中
WantAuthnRequestsSigned="false",说明IdP不要求SP签名认证请求,因此SigningCertificate相关代码无需启用;同时你已经通过元数据自动加载IdP的签名证书到SignatureValidationCertificates,手动加载的SignatureValidationCertificateFile也无需启用。
- 不需要。IdP元数据中
是否必须使用SHA1证书?
- 不需要。你的SHA256自签名证书用于SP侧(如果需要签名请求),和IdP的签名算法无关。元数据中的
signAlgorithm=SHA1是IdP用来签名响应的算法,只需确保SP配置的SignatureAlgorithm和IdP实际使用的签名算法一致即可(当前配置的SHA1是匹配的)。建议后续推动IdP升级到SHA256以提升安全性。
- 不需要。你的SHA256自签名证书用于SP侧(如果需要签名请求),和IdP的签名算法无关。元数据中的
SignIn方法中的NameId是否需要修改?
- 需要移除当前的
Subject配置。发起认证请求时,无需提前指定固定的NameID="abcd",应该让IdP根据用户凭证返回对应的NameID。保留NameIdPolicy配置即可,AllowCreate=true会让IdP为新用户创建持久化ID。
- 需要移除当前的
是否需要修改validationMode?
- 当前
CertificateValidationMode="None"和RevocationMode="NoCheck"适合测试环境或自签名证书场景,签名验证失败和此配置无关(这两个配置是验证证书的信任链,而非签名本身)。生产环境建议改为ChainTrust,但需确保IdP证书在信任链中。
- 当前
SignatureAlgorithm配置的作用?
- 该配置有两个作用:一是当
SignAuthnRequest=true时,用于SP签名认证请求的算法;二是用于验证IdP返回的SAML响应的签名算法。必须和IdP实际使用的签名算法一致,当前配置的SHA1匹配元数据中的参数,是正确的。
- 该配置有两个作用:一是当
排查方向
- 验证IdP公钥正确性:将元数据中的
X509Certificate内容导出为.crt文件,手动加载到SignatureValidationCertificates中,替换元数据自动加载的逻辑,确认是否能通过验证。 - 检查响应签名算法:解析SAML响应XML,确认
<Signature>节点中的算法URI是否为http://www.w3.org/2001/04/xmldsig-more#rsa-sha1,与SP配置一致。 - 确认证书有效期:检查本地服务器时间是否正确,确保从元数据加载的IdP证书未过期(代码中已做
IsValidLocalTime()校验,但可手动确认)。 - 手动验证签名:使用openssl工具,将SAML响应的签名部分与IdP公钥进行手动验证,命令示例:
openssl dgst -sha1 -verify idp-public-key.pem -signature signature.bin response.xml - 排查响应篡改:确保SP与IdP之间使用HTTPS传输,捕获原始SAML响应XML,确认未被篡改。
内容的提问来源于stack exchange,提问作者Nino
相关产品推荐
相关产品推荐

