You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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);
}

疑问解答

  1. 注释的证书代码是否需要取消注释?

    • 不需要。IdP元数据中WantAuthnRequestsSigned="false",说明IdP不要求SP签名认证请求,因此SigningCertificate相关代码无需启用;同时你已经通过元数据自动加载IdP的签名证书到SignatureValidationCertificates,手动加载的SignatureValidationCertificateFile也无需启用。
  2. 是否必须使用SHA1证书?

    • 不需要。你的SHA256自签名证书用于SP侧(如果需要签名请求),和IdP的签名算法无关。元数据中的signAlgorithm=SHA1是IdP用来签名响应的算法,只需确保SP配置的SignatureAlgorithm和IdP实际使用的签名算法一致即可(当前配置的SHA1是匹配的)。建议后续推动IdP升级到SHA256以提升安全性。
  3. SignIn方法中的NameId是否需要修改?

    • 需要移除当前的Subject配置。发起认证请求时,无需提前指定固定的NameID="abcd",应该让IdP根据用户凭证返回对应的NameID。保留NameIdPolicy配置即可,AllowCreate=true会让IdP为新用户创建持久化ID。
  4. 是否需要修改validationMode?

    • 当前CertificateValidationMode="None"和RevocationMode="NoCheck"适合测试环境或自签名证书场景,签名验证失败和此配置无关(这两个配置是验证证书的信任链,而非签名本身)。生产环境建议改为ChainTrust,但需确保IdP证书在信任链中。
  5. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 13:37:02