После обсуждения с коллегой я думаю, что я понял ход действий. Оба OAuthWebSecurity
и WebSecurity
, как представляется, входят в состав SimpleMembership
, поэтому то, что я написал в этом вопросе, указывает на то, что я хочу написать пользовательское членство или реверсный инженер SimpleMembership
для копирования OAuthWebSecurity
(что не похоже на забавную деятельность).
Лучше всего здесь захватить OAuthWebSecurity
, написав пользовательский клиент (тот, который реализует интерфейс IAuthenticationClient
). Обычно регистрируются различные клиенты OAuth с использованием встроенных методов OAuthWebSecurity
(например, RegisterFacebookClient
). Но также можно зарегистрировать этих клиентов, используя OAuthWebSecurity.RegisterClient
, который принимает IAuthenticationClient
. Таким образом, я должен быть в состоянии добавить этот логин SAML без написания пользовательского поставщика членства и продолжать использовать SimpleMembership
.
Мне это удалось. К счастью, поставщик удостоверений не был чрезвычайно сложным, поэтому все, что мне нужно было сделать, это перенаправить на определенный адрес (мне даже не нужно было запрашивать утверждение). После успешного входа в систему IDP «перенаправляет» пользователя, использующего POST на мой сайт, с прикрепленным SAMLResponse с кодировкой base64. Поэтому все, что я должен был сделать, это разобрать и подтвердить ответ. Я поместил код для этого в свой пользовательский клиент (реализующий интерфейс 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 с помощью OAuthWebSecurity.RegisterClient
, а затем я мог повторно использовать мой существующий код внешнего входа (который первоначально был сделан для OAuth). По разным причинам мой обратный вызов SAML был другим действием, чем мой обратный вызов OAUTH. Код для этой акции был более или менее это:
[AllowAnonymous]
public ActionResult Saml(string returnUrl)
{
Session["SAMLResponse"] = Request.Form["SAMLResponse"];
return Redirect(Url.Action("ExternalLoginCallback") + "?__provider__=mySAML");
}
Кроме того OAuthWebSecurity.VerifyAuthentication
не работал с моим клиентом слишком хорошо, так что я должен был условно запустить свою собственную проверку в OAuth обратного вызова.
AuthenticationResult result = null;
if (Request.QueryString["__provider__"] == "mySAML")
{
result = new mySAMLClient().VerifyAuthentication(HttpContext);
}
else
{
// use OAuthWebSecurity.VerifyAuthentication
}
Это, наверное, все выглядит очень странно и может значительно отличаться в случае вашего IDP, но благодаря этому я смог повторно использовать большую часть существующего кода для обработки внешних счетов.
Спасибо за предложение. Я уже смотрел на ваш код для этой цели (что не нужно писать все с нуля). – jahu