Разобрать и проверить XML-токен доверия WS
У меня есть веб-сервис, написанный на C#/.NET, который перенаправляет неаутентифицированных пользователей провайдеру идентификации федерации WS, который затем перенаправляет обратно на мой веб-сервис с токеном SAML, который выполняет роли этого пользователя. Это согласно спецификации пассивной федерации WS - http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html
Получив это, я получаю запрос, в котором wresult установлен как токен. В моем коде у меня есть строковое значение, которое является строкой для XML-документа. То, что я знаю, это сфера, в которой я работаю, отпечаток провайдера идентификации, wctx (если он был отправлен).
Маркер безопасности является стандартным токеном WS-Trust, описанным здесь: http://specs.xmlsoap.org/ws/2005/02/trust/WS-Trust.pdf
Я хочу получить SecurityToken и, в конечном счете, IPrincipal для этого пользователя только из той строки, которая является XML-документом / токеном безопасности.
Пример строки будет (с некоторыми вещами, запутанными).
<?xml version="1.0"?>
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:Lifetime>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-14T13:40:25.164Z</wsu:Created>
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2018-09-14T14:40:25.164Z</wsu:Expires>
</t:Lifetime>
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>https://localhost:44366/</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<t:RequestedSecurityToken>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion" MajorVersion="1" MinorVersion="1" AssertionID="_e1580903-02ac-453d-a157-ae27c8614cc9" Issuer="http://adfs.ORGANISATION.com/adfs/services/trust" IssueInstant="2018-09-14T13:40:25.164Z">
<saml:Conditions NotBefore="2018-09-14T13:40:25.164Z" NotOnOrAfter="2018-09-14T14:40:25.164Z">
<saml:AudienceRestrictionCondition>
<saml:Audience>https://localhost:44366/</saml:Audience>
</saml:AudienceRestrictionCondition>
</saml:Conditions>
<saml:AttributeStatement>
<saml:Subject>
<saml:SubjectConfirmation>
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Attribute AttributeName="emailaddress" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>person@stuff.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="givenname" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>Jeff</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="surname" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>Mandelson</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="windowsaccountname" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
<saml:AttributeValue>jeff.mandelson</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="role" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
<saml:AttributeValue>Stuff\Domain Users</saml:AttributeValue>
<saml:AttributeValue>Stuff\DevTeam</saml:AttributeValue>
<saml:AttributeValue>Stuff\RDS-MSSQLDEV-RW</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="upn" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>stuff@local.com</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="name" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>Jeff Mandelson</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" AuthenticationInstant="2018-09-14T11:59:16.147Z">
<saml:Subject>
<saml:SubjectConfirmation>
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
</saml:SubjectConfirmation>
</saml:Subject>
</saml:AuthenticationStatement>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<ds:Reference URI="#_e1580903-02ac-453d-a157-ae27c8614cc9">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>a_digest_value_removed</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>signature</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>certificate</X509Certificate>
</X509Data>
</KeyInfo>
</ds:Signature>
</saml:Assertion>
</t:RequestedSecurityToken>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
</t:RequestSecurityTokenResponse>
Я пытался использовать встроенные методы, такие как WSFederationAuthenticationModule, однако, похоже, что это проблемы, если вы не используете System.Web.Request. Встроенная функция.NET/C# предпочтительнее!
2 ответа
Решение состоит в том, чтобы думать о токене, как о обычном 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
это документирует подход, который мы используем внутри одного из наших приложений. Я планировал сделать эту запись в течение некоторого времени, и ваш вопрос был всего лишь необходимым спусковым крючком.
Другой способ - использовать IdentityModel SamlTokenVerifier и Parser:
public static bool AuthenticateXmlToken(String wresult)
{
String pstrXML = wresult;
// write it down
File.WriteAllText("C:\\Users\\USER\\Downloads\\asdf4.xml", wresult);
// extract the SAML Assertion
XmlReader reader = XmlReader.Create(new StringReader(pstrXML));
reader.ReadToFollowing("Assertion", "urn:oasis:names:tc:SAML:1.0:assertion");
// saml requirements
SamlSecurityTokenRequirement pRequirements = new SamlSecurityTokenRequirement();
pRequirements.CertificateValidator = new CertificateValidator();
SecurityTokenHandlerConfiguration pConfig = new SecurityTokenHandlerConfiguration();
pConfig.AudienceRestriction = new AudienceRestriction(AudienceUriMode.Never);
pConfig.IssuerNameRegistry = new IssuerNames();
//pRequirements.ValidateAudienceRestriction()
SamlSecurityTokenHandler pHandler = new SamlSecurityTokenHandler(pRequirements);
pHandler.Configuration = pConfig;
SecurityTokenHandlerCollection tokenHandlerCollection = SecurityTokenHandlerCollection.CreateDefaultSecurityTokenHandlerCollection();
SamlSecurityToken token = (SamlSecurityToken)pHandler.ReadToken(reader.ReadSubtree());
ReadOnlyCollection<ClaimsIdentity> pClaims = pHandler.ValidateToken(token);
return pClaims.Count > 0;
}
public class IssuerNames : IssuerNameRegistry
{
public override string GetIssuerName(SecurityToken securityToken)
{
return "Issuer";
throw new NotImplementedException();
}
}
public class CertificateValidator : X509CertificateValidator
{
public override void Validate(X509Certificate2 certificate)
{
if (certificate == null)
{
throw new Exception("certificate is null");
}
if (certificate.Thumbprint.ToLower() != "mythumprint")
{
throw new Exception("X509 certficate is signed with the wrong public key!");
}
}
}