views:

59

answers:

1

I am calling a non-WCF service from a WCF client. The WCF client includes the "MustUnderstand" header attribute set to "1". Here's a typical SOAP request:

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"&gt;
<s:Header>
    <o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"&gt;
        <u:Timestamp u:Id="_0">
            <u:Created>2010-08-23T20:48:52.680Z</u:Created>
            <u:Expires>2010-08-23T20:53:52.680Z</u:Expires>
        </u:Timestamp>
        <o:UsernameToken u:Id="uuid-72ea0c0a-43aa-43b2-bed7-c2da13624105-1">
            <o:Username>blablabla</o:Username>
            <o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"&gt;blablabla&lt;/o:Password&gt;
        </o:UsernameToken>
    </o:Security>
</s:Header>
<s:Body xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"&gt;
    <HeartbeatRequest xmlns="http://removed"&gt;
        <DateTime xmlns="">8/23/2010 4:48:51 PM</DateTime>
        <Message xmlns="">123</Message>
    </HeartbeatRequest>
</s:Body>

Now, here's the response I get back for this.

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"&gt;
<soapenv:Header>
    <Misunderstood qname="o:Security" xmlns="http://www.w3.org/2002/06/soap-faults" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" />
</soapenv:Header>
<soapenv:Body>
    <soapenv:Fault xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"&gt;
        <faultcode>soapenv:MustUnderstand</faultcode>
        <faultstring>WSWS3173E: Error: Did not understand &quot;MustUnderstand&quot; header(s):{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}Security&lt;/faultstring&gt;
    </soapenv:Fault>
</soapenv:Body>

Note the part about MustUnderstand not being understood.

The owner of this service has indicated that they allow elements that have the WSSE namespace prefix but aren't actually in the XSD, and do some other processing that would prevent them from accepting MustUnderstand="1" so I have to find a way to send messages with MustUnderstand="0".

I tried to change this in the MessageContract for the proxy client using a MessageHeader attribute, but that didn't help.

Next, I implemented a custom client message inspector. I created classes per MSDN for a custom Behavior Extension Element and an IEndpointBehavior, these are trivial but here for completeness:

    public class ExClientBehavior : IEndpointBehavior
{
    #region IEndpointBehavior Members

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        // no op
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
        ExInspector inspector = new ExInspector();
        clientRuntime.MessageInspectors.Add(inspector);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
    {
        // no op
    }

    public void Validate(ServiceEndpoint endpoint)
    {
        // no op
    }

    #endregion
}


    public class ExClientBehaviorExtensionElement : BehaviorExtensionElement
{
    public override Type BehaviorType
    {
        get { return typeof(ExClientBehavior); }
    }

    protected override object CreateBehavior()
    {
        return new ExClientBehavior();
    }
}

and now the actual Inspector:

    public class ExInspector : IClientMessageInspector
{

    #region IClientMessageInspector Members

    public void AfterReceiveReply(ref Message reply, object correlationState)
    {
        // no op
        return;
    }

    public object BeforeSendRequest(ref Message request, IClientChannel channel)
    {
        MessageBuffer buffer = request.CreateBufferedCopy(int.MaxValue);

        Message newMessage = buffer.CreateMessage();

        newMessage.Headers.RemoveAt(0);

        newMessage.Headers.Add(MessageHeader.CreateHeader
            (
                request.Headers[0].Name,
                request.Headers[0].Namespace,
                string.Empty,
                false,
                string.Empty,
                request.Headers[0].Relay
            )
        );

        request = newMessage;

        return null;
    }

    #endregion
}

As you see, I am creating a new request via buffered copy, then deleting the security header (there is only one header) and adding a new one with MustUnderstand set to false (why am I doing this? MessageHeader.MustUnderstand is read-only). I set a breakpoint in this method and indeed, the new header is added, the newMessage is written back to the request, and both newMessage.Headers[0].MustUnderstand as well as request.Headers[0].MustUnderstand are false at the end of this method.

However, the message that is sent to the service still includes MustUnderstand="1" on the header!!!!!

Here's the app.config that includes the behavior above:

<configuration>
<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="WebServiceSOAP" closeTimeout="00:01:00" openTimeout="00:01:00"
                    receiveTimeout="00:10:00" sendTimeout="00:01:00" allowCookies="false"
                    bypassProxyOnLocal="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferSize="65536" maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" transferMode="Buffered"
                    useDefaultWebProxy="true">
                <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                <security mode="TransportWithMessageCredential">
                    <transport clientCredentialType="Basic" proxyCredentialType="None" realm="" />
                    <message clientCredentialType="UserName" algorithmSuite="Default" />
                </security>
            </binding>
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint
                address="https://removed"
                behaviorConfiguration="ovrExClientBehavior"
                binding="basicHttpBinding"
                bindingConfiguration="WebServiceSOAP"
                contract="EWebService.EWebService"
                name="WebServiceSOAP" />
    </client>
    <extensions>
        <behaviorExtensions>
            <add name="exClientBehavior" type="ExMessageInspector.ExClientBehaviorExtensionElement, ExMessageInspector, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
        </behaviorExtensions>
    </extensions>
    <behaviors>
        <endpointBehaviors>
            <behavior name="ovrExClientBehavior">
                <exClientBehavior />
            </behavior>
        </endpointBehaviors>
    </behaviors>
</system.serviceModel>

So my question: is it possible to change MustUnderstand on an outgoing message like above, or in a similar manner? Or is it forcibly being changed back to true later in the pipeline, after the inspector replaces the security header?

Note: the service owner says that they are only aware of one other organization consuming this service in .NET, and that that consumer had to essentially throw out WCF and WSE and create SOAP messages - and handle the replies - from scratch, probably using POX POSTs or some such. We would really prefer to avoid this since we need to call a number of operations on the service.

Also, we need to preserve the body and properties of the message intact.

Any help would be really appreciated!!

+1  A: 

I wonder why standards for interoperability exists if vendors do not follow them. If client inspector doesn't work you can try to implement custom message encoder and modify header there.

Edit:

The question here is why do you send user credentials for authentication if you in the same time declaring that service doesn't have to understand the header with credentials = doesn't have to use them. Do you really need them?

There are other approaches dependent on your requirements. Do you need timestamp? Is the timestamp checked on the server? Do you have single user for all calls or do you have to differ users between calls?

If you don't need timestamp or timestamp is not checked and you have only single user name and password the easiest way is not to use TranportWithMessageCredential security mode. Use pure Transport instead and place header description inside client endpoint configuration like:

<client>
   <endpoint address="https://removed" binding="basicHttpBinding" bindingConfiguration="WebServiceSOPA" contract="EWebService.EWebService" name="WebServiceSOAP">
    <headers>
     <wsse:Security s:mustUnderstand="0" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"&gt;
      <wsse:UsernameToken wsu:Id="SecurityToken-3f7f983f-66ce-480d-bce6-170632d33f92" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"&gt;
       <wsse:Username>User</wsse:Username>
       <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"&gt;Pwd123&lt;/wsse:Password&gt;
      </wsse:UsernameToken>
     </wsse:Security>
    </headers>
   </endpoint>
  </client>

If you have several user names or if you need real timestamp with actual data you can use same approach but instead of static configuration you can create custom header in code and avoid WCF security. This can be done using message inspector.

Ladislav Mrnka
Unfortunately yes, this vendor combines MustUnderstand=0 with still mandating the (static) username/password in the header. If we omit that, we get back a missing actor (i.e. missing element) error. I don't think timestamp is required.Hm - I'll try the endpoint config approach and comment back.
pelazem
OK, thank you Ladislav! I tried your approach, and since we are "lucky" enough to have a static username/password, going with Transport only for security and the static header as you laid it out - works!! Thank you very much.
pelazem