Need help with manipulating SOAP header in my WCF

2020-04-11 11:53发布

问题:

I have a unique requirement where I need to send a highly customized soap header in a request to a external vendor. The only way my WCF client can interact with their web service is using a combination of Username token and message signing the entire envelope(see vendor provided soap header below).

   <soapenv:Envelope xmlns:bsvc="urn:com.workday/bsvc"   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Header>
   <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
  <wsse:UsernameToken wsu:Id="UsernameToken-20" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <wsse:Username>Cert509User</wsse:Username>
  </wsse:UsernameToken>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
        <DigestValue>Lx8YS/gC/oTagK0cn2rzGCQcYSSiZC9CKqIFqd/X8zw=</DigestValue>
      </Reference>
    </SignedInfo>
      <SignatureValue>p9Z1inN//gcDH85KFfd3RB6jY9hEy93ZqSj1l+sGakpvTgyivTbD0mDXKMpEwQVxCqtsEP9r78voxjlAbgM5PJyMQsmIxz+KQ45LyaA8dDdA4X4TIJ89dgvacT5PY0rtxJD2u2T5cRvQJ7p9etJL4FcQMI9I6XyU7DcKFOuRehE=</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509Certificate>MIIDuzCCAqOgAwIBAgIQK2RKs3P21+p4XAV83a/QLjANBgkqhkiG9w0BAQUFADCBrjETMBEGCgmSJomT8ixkARkWA2NvbTEaMBgGCgmSJomT8ixkARkWCm1hc3RlcmNhcmQxHTAbBgNVBAoTFE1hc3RlckNhcmQgV29ybGRXaWRlMSQwIgYDVQQLExtHbG9iYWwgSW5mb3JtYXRpb24gU2VjdXJpdHkxNjA0BgNVBAMTLUlURiBNQyBQcm9kdWN0aW9uIE5ldHdvcmsgQXBwbGljYXRpb25zIHN1YiBDQTAeFw0xMTA4MDQwOTQwNDlaFw0xNTA4MDMwOTA2NDRaMIGoMQswCQYDVQQGEwJVUzERMA8GA1UECBMITWlzc291cmkxFDASBgNVBAcTC1NhaW50IExvdWlzMTQwMgYDVQQKEytNYXN0ZXJDYXJkIFdvcmxkV2lkZSAtIENvbW1vbiBQcm9kSW5mcmEgU1NMMREwDwYDVQQLEwhzaWduaW5nMTEnMCUGA1UEAxMec3RhZ2Uud29ya2RheUhSLm1hc3RlcmNhcmQuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCt4MlJCVNcmXiQIg8pxR4JsR0QpIuBCPadIAo849CRLpZglIKRWrTlxRIBC2YQeW3OkuDEdqYU6wJzn9m6GHTbmOSAy21aVR0eOqQLHltXytdzOJG92HW1IlBVuzwmMKwzEUjhVatLRQjKvTs6TjJ7egfzO8H2yolU59fq/zLcpQIDAQABo10wWzAfBgNVHSMEGDAWgBQCt+lVDTcnQt+zKa7QBi4/hEiVUzAJBgNVHRMEAjAAMA4GA1UdDwEB/wQEAwIHgDAdBgNVHQ4EFgQUM23TyPCInFlw2PnukzGOn8kKldcwDQYJKoZIhvcNAQEFBQADggEBAJeAcKk3YWN12frCQSuKzO4qTBNo+QjUjXEHfYuUl8i2pJHs6tDuDkX36RYPWyXLyMPXHSOoomlVmsCprGLqfTGBf1jW/e7Re3sg3/k1iJFg3f1mMKxGP0MuUvuofc/Nj+ezvvl/Nswn3bsAMgvktM+OR5KEhi293qlix87mpvmuvDUw1ZfoQpgN8AvdiQiRWBN2SXahwzGJo+gRjy6EUGdNgc+lsPDkkKxF6csWsb59yip4t7nTbSjqi5XCjZYfMAG5cDhDELtqge5i1W+1a0mP12xKb5P205HSjH9jF/N67CwOBxuuUXaexsqbLaRfL0Dxo0oFwusnIQ1A2qMgg1c=</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>
</wsse:Security>
</soapenv:Header>
<soapenv:Body xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
SOAP BODY goes HERE
</soapenv:Body>
</soapenv:Envelope>

I have tried combination of wshttpbinding, custom binding with different behaviors in “app.config”. I was unable to replicate the SOAP header shown above nor was I able connect to web service. It is just the way the header needs to be setup that I cannot configure thru app.config. So I asked my vendor on how I can replicate the header within the WCF client. They sent me a block of code(x509 Authentication.cs) that they tested and confirmed that it works(no idea how it worked for them). Basically I “somehow” need to intercept the request as my WCF client sends the request to the vendor , after intercepting it somehow pass the SOAP body as an input to a method ( CreateX509SoapEnvelope(“SOAP body”)).I have Attached complete code x509 Authentication.cs below

    class x509_Authentication
     {
      public string CreateX509SoapEnvelope(string xml)
      {
        string soapXML;
        soapXML = "<soapenv:Envelope xmlns:bsvc=\"urn:com.workday/bsvc\" 
                   xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">";
        soapXML += "<soapenv:Header>\n";

        // Add security block for X.509 certificate
        soapXML = "<wsse:Security xmlns:wsse=\"http://docs.oasis-
                   open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">";
        soapXML += "<wsse:UsernameToken wsu:Id=\"UsernameToken-20\"     
                    xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-
                     wss-wssecurity-utility-1.0.xsd\">";
        soapXML += "<wsse:Username>Cert509User</wsse:Username>";
        soapXML += "</wsse:UsernameToken>";
        soapXML += "</wsse:Security>";

        soapXML += "</soapenv:Header>" + xml + "</soapenv:Envelope>";

        // Sign Envelope
        soapXML = CreateSignatureBlock(soapXML, "wsse:Security");

        // Verify that the XML was signed properly
        VerifySignedXml(soapXML);

        return soapXML;
    }

    public string CreateSignatureBlock(string xml, string sParentSignatureTagName)
    {
        try
        {
            string certificatePath="C:\\Users\\user3434\\Desktop\\certfolder\\cert.p12";
            //load xml into a dom
            XmlDocument xd = new XmlDocument();
            xd.LoadXml(xml);

            // Set Certificate
            System.Security.Cryptography.X509Certificates.X509Certificate2 cert = new X509Certificate2(certificatePath, "changeit");
            //System.Security.Cryptography.X509Certificates.X509Certificate2 cert = x509_Authentication.GetCertificateFromStore();
            SignedXml signedXml = new SignedXml(xd);
            signedXml.SigningKey = cert.PrivateKey;

            // Create a new KeyInfo object.
            KeyInfo keyInfo = new KeyInfo();
            keyInfo.Id = "";

            // Load the certificate into a KeyInfoX509Data object
            // and add it to the KeyInfo object.
            KeyInfoX509Data keyInfoData = new KeyInfoX509Data();
            keyInfoData.AddCertificate(cert);
            keyInfo.AddClause(keyInfoData);

            // Add the KeyInfo object to the SignedXml object.
            signedXml.KeyInfo = keyInfo;

            // Need to use External Canonicalization method.
            signedXml.SignedInfo.CanonicalizationMethod = "http://www.w3.org/2001/10/xml-exc-c14n#";

            // Create a reference to be signed.
            Reference reference = new Reference();
            reference.Uri = "";

            // Add an enveloped transformation to the reference.
            XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();
            reference.AddTransform(env);
            reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";

            // Add the reference to the SignedXml object.
            signedXml.AddReference(reference);

            // Add the Signature Id
            signedXml.Signature.Id = "";

            // Compute the signature.
            signedXml.ComputeSignature();

            // Get the XML representation of the signature and save
            // it to an XmlElement object.
            XmlElement xmlDigitalSignature = signedXml.GetXml();

            // Append the Signature element to the XML document. It will find the element after which we want to insert the signature
            XmlNodeList nodeList = xd.GetElementsByTagName(sParentSignatureTagName);
            if (nodeList.Count > 0)
            {
                XmlNode headerNode = nodeList[0];
                headerNode.AppendChild(xd.ImportNode(xmlDigitalSignature, true));
            }

            return xd.InnerXml;
        }
        catch
        {
            return xml;
        }
    }

    public void VerifySignedXml(String xml)
    {

        // Create a new XML document.
        XmlDocument xmlDocument = new XmlDocument();
        xmlDocument.PreserveWhitespace = true;
        xmlDocument.LoadXml(xml);

        // Create a new SignedXml object and pass it
        // the XML document class.
        SignedXml signedXml = new SignedXml(xmlDocument);

        // Add an enveloped transformation to the reference.
        XmlDsigEnvelopedSignatureTransform env = new XmlDsigEnvelopedSignatureTransform();

        Reference reference = new Reference();
        reference.AddTransform(env);
        signedXml.AddReference(reference);

        // Find the "Signature" node and create a new XmlNodeList object.
        XmlNodeList nodeList = xmlDocument.GetElementsByTagName("Signature");

        // Load the signature node.
        signedXml.LoadXml((XmlElement)nodeList[0]);

        // Check the signature and return the result.
        //if (!signedXml.CheckSignature(cert,true))
        //if (!signedXml.CheckSignature(new X509Certificate2(certificatePath, "sdfdf"), true))
        //{
        //    log.Error("Invalid Signature");
        //}
    }

}

}

The code accepts soap body & stitches together the custom header and signs the entire envelope returning header as a string. I need to take this string and pass it back to the request and send it on its way to the vendor. It sounds way too complicated for me theoritically. But with research I found that there was a way to trap the outgoing message by implementing the IClientMessageInspector interface and overriding the “BeforeSendRequest” method. I got the part of the code working where when WCF client is executed the “BeforeSendRequest” method is being called. But now I am stuck at how to extract the SOAP body from the outgoing message (I see the body when I debug) and send it to as an input to Createx509Envelope method and then take the output of the method and put it back in the “request” object and send the message to the vendor .. see my BeforeSendRequest method implementation(nothing much in there I am stuck)

    public string RequestMessage { get; set; }
        public string ResponseMessage { get; set; }


  object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)
        {

            **HOW TO EXTRACT SOAPBODY FROM “.ServiceModel.Channels.Message request” OBJECT and CONVERT THAT TO STRING ??????????**

x509_Authentication x509 = new x509_Authentication();
            this.ResponseMessage = x509.CreateX509SoapEnvelope(SOAP Body);

**TAKE THE RESPONSE MESSAGE AND CONVERT BACK TO “.ServiceModel.Channels.Message request” AND SEND THE REQUEST ALONG???????????**

            return null;
        }

If there is better way of implementing it ? please provide samples.. this is the first where I had to send a custom SOAP header to vendor and its complicated for me. Running short on time. PLEASE HELP!!!!!!

回答1:

Check out the following article which shows you an example of how to implement an IClientMessageInspector which alters the message and injects a custom header.

Handling custom SOAP headers via WCF Behaviors

First you need to define a custom header to represent the contents of a SOAP header. To do so create your own descendant of the MessageHeader class.

public class MyHeader : MessageHeader
{ 
    //... 
}

Create an IClientMessageInspector implementation that injects your custom header just before sending the request (BeforeSendRequest).

public class CustomMessageInspector : IClientMessageInspector
{
    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
        request = buffer.CreateMessage();
        request.Headers.Add(new MyHeader());
        return null;
    }

    //...
}

Now you need to add your custom message inspector to the WCF pipeline, but you already got this part covered.

The Message parameter of the BeforeSendRequest(ref Message request, IClientChannel channel) can be used to read the SOAP message using one of the methods of the Message type (ToString(), GetBody(), GetReaderAtBodyContents()...etc.).

To get the body of the message use the GetReaderAtBodyContents() method which returns an XmlDictionaryReader object. Use this XML reader to retrieve the body as a string.

For example:

using (XmlDictionaryReader reader = message.GetReaderAtBodyContents())
{
    string content = reader.ReadOuterXml();
    //...   
}