简体   繁体   English

解析并验证WS Trust XML令牌

[英]Parse and verify a WS Trust XML Token

I have a webservice written in c#/.NET that redirects unauthenticated users to a WS Federation identity provider, which then redirects back to my webservice with a SAML token which has the roles of that user. 我有一个用c#/。NET编写的Web服务,该服务将未经身份验证的用户重定向到WS Federation身份提供程序,然后使用具有该用户角色的SAML令牌将其重定向回到我的Web服务。 This is as per the passive WS federation specification - http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html#_Toc223175008 根据被动WS联合身份验证规范-http://docs.oasis-open.org/wsfed/federation/v1.2/os/ws-federation-1.2-spec-os.html#_Toc223175008

Having got this, I get a request which has the wresult set to be the token. 有了这个,我得到一个请求,该请求的结果设置为令牌。 In my code I've got a string wresult which is the string for the xml document. 在我的代码中,我有一个字符串 wresult,它是xml文档的字符串。 What I know is the realm im on, the thumbprint of the identity provider, the wctx (if it was sent). 我所知道的是领域,身份提供者的指纹,wctx(如果已发送)。

The security token is a standard WS-Trust token described here: http://specs.xmlsoap.org/ws/2005/02/trust/WS-Trust.pdf 安全令牌是此处描述的标准WS-Trust令牌: http : //specs.xmlsoap.org/ws/2005/02/trust/WS-Trust.pdf

What I want to get is the SecurityToken and eventually the IPrincipal for that user just from that string which is the XML document/security token. 我想从该字符串 (即XML文档/安全令牌)中获取该用户的SecurityToken和最终的IPrincipal。

An example of the string would be (with a few things obfuscated). 字符串的一个示例是(混淆了一些内容)。

<?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>

I've tried using the inbuilt methods such as WSFederationAuthenticationModule, however, this seems to have problems unless you're using System.Web.Request. 我尝试使用WSFederationAuthenticationModule之类的内置方法,但是,除非您使用System.Web.Request,否则这似乎有问题。 An inbuilt .NET/C# function would be preferable! 内置的.NET / C#函数将是更好的选择!

The solution is to think of the token as it was a regular XMLDsig signed XML - the assertion node is signed and the signature's reference points back to it. 解决方案是将令牌视为常规XMLDsig签名的XML-声明节点已签名,并且签名的引用指向该令牌。 The code is rather simple, what's interesting however is that the SignedXml class has to be inherited to have the signature validator that follows the AssertionID attribute (the default convention is that the signed node's id attribute is called just ID and the default validator just won't find the node that has the id attribute called differently). 代码很简单,但是有趣的是,必须继承SignedXml类,以使签名验证器遵循AssertionID属性(默认约定是,已签名节点的id属性仅称为ID ,而默认验证器将SignedXml 。找不到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;
    }
}

Note that the XPath assumes the token has the RequestSecurityTokenResponseCollection in the root, make sure your tokens follow this convention (in case of a single token, the collection node can be missing and the token's root could be just RequestSecurityTokenResponse , update the code accordingly). 请注意,XPath假设令牌的根中具有RequestSecurityTokenResponseCollection ,请确保您的令牌遵循此约定(如果是单个令牌,则可能会丢失集合节点,并且令牌的根可能只是RequestSecurityTokenResponse ,请相应地更新代码)。

The validation code is then 然后输入验证码

// 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;
}

With some minor tweaks, you should be able to do what you want. 进行一些细微的调整,您应该可以做自己想做的事情。

BTW. BTW。 Most of the answer is copied from my blog entry 大部分答案是从我的博客条目中复制的

https://www.wiktorzychla.com/2018/09/parsing-saml-11-ws-federation-tokens.html https://www.wiktorzychla.com/2018/09/parsing-saml-11-ws-federation-tokens.html

that documents the approach we are using internally in one of our apps. 该文档记录了我们在其中一个应用程序内部使用的方法。 I planned to make this entry for some time and your question was just a trigger I needed. 我计划输入一段时间,而您的问题只是我需要的一个触发条件。

Another way is to use the IdentityModel SamlTokenVerifier and Parser: 另一种方法是使用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!");
                }
            }
        }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM