Решение состоит в том, чтобы думать о токене, как о обычном XMLDsig-подписанном XML - узел подтверждения подписан, а ссылка на подпись указывает на него.Код довольно прост, однако интересно то, что класс SignedXml
должен быть унаследован, чтобы иметь валидатор подписи, следующий за атрибутом AssertionID
(соглашение по умолчанию состоит в том, что атрибут id подписанного узла называется просто ID
ивалидатор по умолчанию просто не найдет узел с атрибутом id, называемым по-другому).
public class SamlSignedXml : SignedXml
{
public SamlSignedXml(XmlElement e) : base(e) { }
public override XmlElement GetIdElement(XmlDocument document, string idValue)
{
XmlNamespaceManager mgr = new XmlNamespaceManager(document.NameTable);
mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");
XmlElement assertionNode =
(XmlElement)document.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/"+
"trust:RequestedSecurityToken/saml:Assertion", mgr);
if (assertionNode.Attributes["AssertionID"] != null &&
string.Equals(assertionNode.Attributes["AssertionID"].Value, idValue, StringComparison.InvariantCultureIgnoreCase)
)
return assertionNode;
return null;
}
}
Обратите внимание, что XPath предполагает, что токен имеет RequestSecurityTokenResponseCollection
в корне, убедитесь, что ваши токены следуют этомусоглашение (в случае одного токена узел сбора может отсутствовать, а корень токена может быть просто RequestSecurityTokenResponse
, обновите код соответствующим образом).
Код проверки тогда будет
// token is the string representation of the SAML1 token
// expectedCertThumb is the expected certificate's thumbprint
protected bool ValidateToken( string token, string expectedCertThumb, out string userName )
{
userName = string.Empty;
if (string.IsNullOrEmpty(token)) return false;
var xd = new XmlDocument();
xd.PreserveWhitespace = true;
xd.LoadXml(token);
XmlNamespaceManager mgr = new XmlNamespaceManager(xd.NameTable);
mgr.AddNamespace("trust", "http://docs.oasis-open.org/ws-sx/ws-trust/200512");
mgr.AddNamespace("wsu", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
mgr.AddNamespace("saml", "urn:oasis:names:tc:SAML:1.0:assertion");
// assertion
XmlElement assertionNode = (XmlElement)xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/saml:Assertion", mgr);
// signature
XmlElement signatureNode = (XmlElement)xd.GetElementsByTagName("Signature")[0];
var signedXml = new SamlSignedXml( assertionNode );
signedXml.LoadXml(signatureNode);
X509Certificate2 certificate = null;
foreach (KeyInfoClause clause in signedXml.KeyInfo)
{
if (clause is KeyInfoX509Data)
{
if (((KeyInfoX509Data)clause).Certificates.Count > 0)
{
certificate =
(X509Certificate2)((KeyInfoX509Data)clause).Certificates[0];
}
}
}
// cert node missing
if (certificate == null) return false;
// check the signature and return the result.
var signatureValidationResult = signedXml.CheckSignature(certificate, true);
if (signatureValidationResult == false) return false;
// validate cert thumb
if ( !string.IsNullOrEmpty( expectedCertThumb ) )
{
if ( !string.Equals( expectedCertThumb, certificate.Thumbprint ) )
return false;
}
// retrieve username
// expires =
var expNode = xd.SelectSingleNode("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:Lifetime/wsu:Expires", mgr );
DateTime expireDate;
if (!DateTime.TryParse(expNode.InnerText, out expireDate)) return false; // wrong date
if (DateTime.UtcNow > expireDate) return false; // token too old
// claims
var claimNodes =
xd.SelectNodes("//trust:RequestSecurityTokenResponseCollection/trust:RequestSecurityTokenResponse/trust:RequestedSecurityToken/"+
"saml:Assertion/saml:AttributeStatement/saml:Attribute", mgr );
foreach ( XmlNode claimNode in claimNodes )
{
if ( claimNode.Attributes["AttributeName"] != null &&
claimNode.Attributes["AttributeNamespace"] != null &&
string.Equals( claimNode.Attributes["AttributeName"].Value, "name", StringComparison.InvariantCultureIgnoreCase ) &&
string.Equals( claimNode.Attributes["AttributeNamespace"].Value, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims", StringComparison.InvariantCultureIgnoreCase ) &&
claimNode.ChildNodes.Count == 1
)
{
userName = claimNode.ChildNodes[0].InnerText;
return true;
}
}
return false;
}
С некоторыми незначительными изменениями вы сможете делать то, что хотите.
Кстати.Большая часть ответа скопирована из моей записи в блоге
https://www.wiktorzychla.com/2018/09/parsing-saml-11-ws-federation-tokens.html
, которая документирует подход, который мы используем внутри нашего приложения.Я планировал сделать эту запись в течение некоторого времени, и ваш вопрос был всего лишь необходимым триггером.