1

我已经使用 itfoxtec 的 SAML2 库在我的 ASP.NET MVC 应用程序中实现了一个 SP。我正在使用 samltest.id 作为 IdP 进行测试。IdP 启动的工作流完美运行,但 SP 启动的工作流总是从 samltest.id 收到 400 错误。我试图查看 samltest.id 的日志,以查看那里是否为我的请求记录了错误,但我似乎在那里找不到任何东西。

这是处理用户在启动 SSO 时将访问的 URL 的操作:

public ActionResult SSOLogin() {
    LogManager logger = new LogManager("SSOLogin");

    string hostname = this.GetHostname();
    SchoolSettings settings = this.GetClientSettings();

    if (settings.UseSAMLSSO) {
        Saml2Configuration samlConfig = null;
        try {
            samlConfig = SamlConfigLoader.GetSaml2Config(HttpContext, settings, this.IsSandbox());
        } catch (Exception e) {
            logger.exception($"loading Saml2Configuration for {hostname}", e);
        }

        if (samlConfig != null) {
            try {
                var binding = new Saml2RedirectBinding();

                binding.SetRelayStateQuery(new Dictionary<string, string> { { "Home/Index", Url.Content("~/") } });

                return binding.Bind(new Saml2AuthnRequest(samlConfig) {

                }).ToActionResult();
            } catch (Exception e) {
                logger.error($"Exception redirecting to IdP. {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
                ViewBag.ssoerror = $"Error redirecting to IdP for {hostname}";
            }
        } else {
            logger.critical($"Could not load SAML2 configuration for {hostname}");
            ViewBag.ssoerror = $"Could not load SAML2 configuration for {hostname}";
        }
    } else {
        ViewBag.ssoerror = "SSO is not configured for this client. Please contact Support";
    }

    return Redirect("/Home/SSOError");
}

加载特定于客户端的元数据的方法如下所示:

public static Saml2Configuration GetSaml2Config(HttpContextBase context, SchoolSettings forSchool, bool forSandbox) {
    LogManager log = new LogManager("getSaml2Config");

    Saml2Configuration config = new Saml2Configuration();
    if (!forSandbox) {
        config.Issuer = _saml2Issuer;
    } else {
        config.Issuer = _saml2IssuerSandbox;
    }

    config.SignatureAlgorithm = _saml2SignatureAlgo;
    config.CertificateValidationMode = X509CertificateValidationMode.None;
    config.RevocationMode = (X509RevocationMode)Enum.Parse(typeof(X509RevocationMode), ConfigurationManager.AppSettings["Saml2:RevocationMode"]);
    config.AllowedAudienceUris.Add(config.Issuer);

    var entityDescriptor = new EntityDescriptor();
    if (forSchool.SAMLMetadataLocationIsUrl) {
        try {
            entityDescriptor.ReadIdPSsoDescriptorFromUrl(new Uri(forSchool.SAMLMetadataLocation));
        } catch (Exception e) {
            log.error($"Exception caught loading metadata from school {forSchool.Hostname} at URL {forSchool.SAMLMetadataLocation}\n Exception {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
            entityDescriptor.IdPSsoDescriptor = null;
        }
    } else {
        var schoolMetadataPath = context.Server.MapPath("~/App_Data/SAMLMetadata/" + forSchool.SAMLMetadataLocation);
        log.info($"Loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}");
        try {
            entityDescriptor.ReadIdPSsoDescriptorFromFile(schoolMetadataPath);
        } catch (IOException ioe) {
            log.error($"IOException caught loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}: {ioe.Message}\n{ioe.StackTrace}");
            entityDescriptor.IdPSsoDescriptor = null;
        } catch (Exception e) {
            log.error($"Exception caught loading metadata for school {forSchool.Hostname} from file {schoolMetadataPath}\n Exception {e.GetType().ToString()}: {e.Message}\n{e.StackTrace}");
            entityDescriptor.IdPSsoDescriptor = null;
        }
    }

    if (entityDescriptor.IdPSsoDescriptor != null) {
        if (entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.Count() > 0) {
            config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
        } else {
            log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SingleSignOnServices that could be parsed.");
        }

        if (entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.Count() > 0) {
            config.SingleLogoutDestination = entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.First().Location;
        } else {
            log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SingleLogoutServices that could be parsed.");
        }

        if (entityDescriptor.IdPSsoDescriptor.SigningCertificates.Count() > 0) {
            config.SignatureValidationCertificates.AddRange(entityDescriptor.IdPSsoDescriptor.SigningCertificates);
        } else {
            log.error($"WARNING: metadata for {forSchool.Hostname} does not have any SigningCertificates that could be parsed.");
        }                
    } else {
        throw new Exception("IdPSsoDescriptor not loaded from metadata.");
    }

    return config;
}

如果有助于澄清情况,我可以添加 AssertionConsumerService 操作的代码,该操作在 IdP 启动的场景中完美运行。

4

2 回答 2

1

我发现了问题。它归结为 GetSaml2Config 方法中的这行代码:

config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;

这天真地采用元数据中的第一个 SingleSignOnService 元素并确定它是正确使用的元素,但这种假设并不总是正确的。我真正想要的是获得一个用于 HTTP-POST 绑定的 SingleSignOnService 元素:

config.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.Where(s => s.Binding.ToString().IndexOf("HTTP-POST") > 0).FirstOrDefault()?.Location;

这适用于我从那以后发现的所有案例。

于 2020-02-23T16:39:05.473 回答
0

您的代码看起来正确。

这可能是一个集成问题,但如果 IdP 不记录错误消息,则很难找到。

你得到什么错误状态消息而不是成功,也许这告诉你一些事情。

也许 IdP 不接受 SAML 2.0 Authn 响应,这里有一些东西要寻找:

  • config.SingleSignOnDestination 可能是必需的
  • Meybe IdP 请求签名请求
  • 也可以在请求中添加其他属性,IdP 文档是否描述了任何要求?
于 2020-02-17T09:45:57.433 回答