我的任务是通过外部服务(使用SAML 2.0)将日志记录添加到使用SimpleMembership的MVC应用程序(.Net 4.5)。说实话,我甚至不确定从哪里开始。从我在互联网上发现的问题来看,问题很少。我发现的大多数材料都涉及与SAML身份提供者的通信(经常从头开始编写)。然而,在我达到这一点之前,我需要确保我能够将其与我们正在使用的SimpleMembership
进行实际整合。
我怀疑对于初学者我需要类似SAMLWebSecurity
的东西(类似于我们也使用的OAuthWebSecurity
)。我在互联网上找不到这样的东西*让我相信它不存在(虽然我不会在这里错了)。这让我相信我必须自己写,但是我可以这样做而不必写自己的会员提供者吗?
*我不确定调用此静态类的正确方法是什么。
答案 0 :(得分:2)
在与同事讨论后,我想我已经找到了行动方案。 OAuthWebSecurity
和WebSecurity
似乎都是SimpleMembership
的一部分,因此我在问题中写的内容表明我想要编写自定义成员资格或反向工程SimpleMembership
来复制OAuthWebSecurity
(听起来不像是一项有趣的活动)。
我最好的选择是通过编写自定义客户端(实现OAuthWebSecurity
接口的客户端)来劫持IAuthenticationClient
。通常,使用OAuthWebSecurity
的内置方法(如RegisterFacebookClient
)注册各种OAuth客户端。但也可以使用接受OAuthWebSecurity.RegisterClient
的{{1}}注册这些客户端。这样我就可以在不编写自定义成员资格提供程序的情况下添加此SAML登录,并继续使用IAuthenticationClient
。
我设法做到了这一点。值得庆幸的是,身份提供者并不是非常复杂,所以我所要做的就是重定向到某个地址(我甚至不需要请求断言)。成功登录后,IDP会使用POST将用户“重定向”到我的网站,并附带base64编码的SAMLResponse。所以我所要做的就是解析并验证响应。我将此代码放在我的自定义客户端中(实现SimpleMembership
接口)。
IAuthenticationClient
然后我使用public class mySAMLClient : IAuthenticationClient
{
// I store the IDP certificate in App_Data
// This can by actually skipped. See VerifyAuthentication for more details
private static X509Certificate2 certificate = null;
private X509Certificate2 Certificate
{
get
{
if (certificate == null)
{
certificate = new X509Certificate2(Path.Combine(HttpContext.Current.ApplicationInstance.Server.MapPath("~/App_Data"), "idp.cer"));
}
return certificate;
}
}
private string providerName;
public string ProviderName
{
get
{
return providerName;
}
}
public mySAMLClient()
{
// This probably should be provided as a parameter for the constructor, but in my case this is enough
providerName = "mySAML";
}
public void RequestAuthentication(HttpContextBase context, Uri returnUrl)
{
// Normally you would need to request assertion here, but in my case redirecting to certain address was enough
context.Response.Redirect("IDP login address");
}
public AuthenticationResult VerifyAuthentication(HttpContextBase context)
{
// For one reason or another I had to redirect my SAML callback (POST) to my OAUTH callback (GET)
// Since I needed to retain the POST data, I temporarily copied it to session
var response = context.Session["SAMLResponse"].ToString();
context.Session.Remove("SAMLResponse");
if (response == null)
{
throw new Exception("Missing SAML response!");
}
// Decode the response
response = Encoding.UTF8.GetString(Convert.FromBase64String(response));
// Parse the response
var assertion = new XmlDocument { PreserveWhitespace = true };
assertion.LoadXml(response);
//Validating signature based on: http://stackoverflow.com/a/6139044
// adding namespaces
var ns = new XmlNamespaceManager(assertion.NameTable);
ns.AddNamespace("samlp", @"urn:oasis:names:tc:SAML:2.0:protocol");
ns.AddNamespace("saml", @"urn:oasis:names:tc:SAML:2.0:assertion");
ns.AddNamespace("ds", @"http://www.w3.org/2000/09/xmldsig#");
// extracting necessary nodes
var responseNode = assertion.SelectSingleNode("/samlp:Response", ns);
var assertionNode = responseNode.SelectSingleNode("saml:Assertion", ns);
var signNode = responseNode.SelectSingleNode("ds:Signature", ns);
// loading the signature node
var signedXml = new SignedXml(assertion.DocumentElement);
signedXml.LoadXml(signNode as XmlElement);
// You can extract the certificate from the response, but then you would have to check if the issuer is correct
// Here we only check if the signature is valid. Since I have a copy of the certificate, I know who the issuer is
// So if the signature is valid I then it was sent from the right place (probably).
//var certificateNode = signNode.SelectSingleNode(".//ds:X509Certificate", ns);
//var Certificate = new X509Certificate2(System.Text.Encoding.UTF8.GetBytes(certificateNode.InnerText));
// checking signature
bool isSigned = signedXml.CheckSignature(Certificate, true);
if (!isSigned)
{
throw new Exception("Certificate and signature mismatch!");
}
// If you extracted the signature, you would check the issuer here
// Here is the validation of the response
// Some of this might be unnecessary in your case, or might not be enough (especially if you plan to use SAML for more than just SSO)
var statusNode = responseNode.SelectSingleNode("samlp:Status/samlp:StatusCode", ns);
if (statusNode.Attributes["Value"].Value != "urn:oasis:names:tc:SAML:2.0:status:Success")
{
throw new Exception("Incorrect status code!");
}
var conditionsNode = assertionNode.SelectSingleNode("saml:Conditions", ns);
var audienceNode = conditionsNode.SelectSingleNode("//saml:Audience", ns);
if (audienceNode.InnerText != "Name of your app on the IDP")
{
throw new Exception("Incorrect audience!");
}
var startDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotBefore"].Value, XmlDateTimeSerializationMode.Utc);
var endDate = XmlConvert.ToDateTime(conditionsNode.Attributes["NotOnOrAfter"].Value, XmlDateTimeSerializationMode.Utc);
if (DateTime.UtcNow < startDate || DateTime.UtcNow > endDate)
{
throw new Exception("Conditions are not met!");
}
var fields = new Dictionary<string, string>();
var userId = assertionNode.SelectSingleNode("//saml:NameID", ns).InnerText;
var userName = assertionNode.SelectSingleNode("//saml:Attribute[@Name=\"urn:oid:1.2.840.113549.1.9.1\"]/saml:AttributeValue", ns).InnerText;
// you can also extract some of the other fields in similar fashion
var result = new AuthenticationResult(true, ProviderName, userId, userName, fields);
return result;
}
}
在App_Start \ AuthConfig.cs中注册了我的客户端,然后我可以重用我现有的外部登录代码(最初是为OAUTH制作的)。由于各种原因,我的SAML回调与我的OAUTH回调不同。此操作的代码或多或少是这样的:
OAuthWebSecurity.RegisterClient
另外[AllowAnonymous]
public ActionResult Saml(string returnUrl)
{
Session["SAMLResponse"] = Request.Form["SAMLResponse"];
return Redirect(Url.Action("ExternalLoginCallback") + "?__provider__=mySAML");
}
对我的客户端不起作用,所以我必须在OAUTH回调中有条件地运行我自己的验证。
OAuthWebSecurity.VerifyAuthentication
这可能看起来非常奇怪,并且在您的IDP情况下可能会有很大差异,但由于这个原因,我能够重用大部分现有代码来处理外部帐户。
答案 1 :(得分:2)
我建议您升级到ASP.NET Identity和基于OWIN的身份验证中间件。然后,您可以使用适用于ASP.NET身份的Kentor.AuthServices中间件(除了必须在解析bug #127之前注释掉XSRF-guard之外)。
如果你必须坚持使用SimpleMembership,你也可以使用Kentor.AuthServices的SAML类,这样你就不必从头开始实现SAML。
免责声明:我是Kentor.AuthServices的作者,但由于它是开源的,我不会为使用它的人赚钱。