Tuesday, June 08, 2010

WCF, TransportWithMessageCredential and MustUnderstands

Problem:
I had to consume a SOAP 1.1 web-service that was being hosted by Oracle’s Enterprise Service Bus (ESB) using WCF. The web-service was setup with transport security (Http over SSL) and message level security using a UsernameToken.

The above configuration should map to a basicHttpBinding (Soap11) and a security mode of “TransportWithMessageCredential” in WCF. This is what that configuration would look like:

<basicHttpBinding>
                <binding name="basicBinding">
                    <security mode="TransportWithMessageCredential">
                        <transport clientCredentialType="None" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="UserName" algorithmSuite="Default" />
                    </security>
                </binding>
</basicHttpBinding>

But when I attempted to use the web-service using this configuration – I got an error with the following message:

“SOAP must understand error:{http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}Security, {http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd}Security.”

When I looked at the request message in Fiddler, I found that the mustUnderstand header was being set to true (or 1).

image

Obviously, the first thing for me to try was to set the mustUnderstand value to 0 or false. Trying this using Fiddler’s response builder, allowed me to confirm that this was indeed true. Unfortunately WCF will not directly allow you to change the mustUnderstand value of the security header. WCF adheres a lot more strictly to the W3C recommendations and hence will always set the mustUnderstand to true on the Security header (as it wants the server to throw a fault if it does not understand the Security header’s contents).

The only way that I could find around the “mustUnderstand” issue is to intercept the message just before the client sends it off to the server and modify the security header manually. I hate this solution as it looks very hacky and would love to hear from anybody else who figured out a much more graceful solution around this.

Solution: WCF messages can be intercepted by any object that implements the IClientMessageInspector interface. In turn this inspector is registered with an end-point using a class that implements the IEndpointBehavior interface. (The inspector is registered in the ApplyClientBehavior method of the interface).

Here is the code for IEndpointBehavior and IClientMessageInspector classes that allows you to modify the headers so that you can add a Security header without the mustUnderstand value set to true.

using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
using System.Xml;

public class MessageBehavior : IEndpointBehavior
{
    string _username;
    string _password;

    public MessageBehavior(string username, string password)
    {
        _username = username;
        _password = password;
    }

    void IEndpointBehavior.AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    { }

    void IEndpointBehavior.ApplyClientBehavior(System.ServiceModel.Description.ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
    {
        clientRuntime.MessageInspectors.Add(new MessageInspector(_username, _password));
    }
    void IEndpointBehavior.ApplyDispatchBehavior(System.ServiceModel.Description.ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
    { }
    void IEndpointBehavior.Validate(System.ServiceModel.Description.ServiceEndpoint endpoint)
    { }

}

public class MessageInspector : IClientMessageInspector
{
    string _username;
    string _password;

    public MessageInspector(string username, string password)
    {
        _username = username;
        _password = password;
    }


    void IClientMessageInspector.AfterReceiveReply(ref System.ServiceModel.Channels.Message reply,
        Object correlationState)
    {
    }

    object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
    {
        request.Headers.Clear();
        string headerText = "<wsse:UsernameToken xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">" +
                                "<wsse:Username>{0}</wsse:Username>" +
                                "<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText\">" +
                                "{1}</wsse:Password>" +
                            "</wsse:UsernameToken>";

        headerText = string.Format(headerText, _username, _password);

        XmlDocument MyDoc = new XmlDocument();
        MyDoc.LoadXml(headerText);
        XmlElement myElement = MyDoc.DocumentElement;

        System.ServiceModel.Channels.MessageHeader myHeader = MessageHeader.CreateHeader("Security", "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", myElement, false);
        request.Headers.Add(myHeader);

        return Convert.DBNull;
    }
}

The things to notice are:

  • In the MessageBehavior class, I register the MessageInspector in the “ApplyClientBehavior” method.
  • The message headers are transformed in the “BeforeSendRequest” method of the MessageInspector class. The headerText contains the header just as I would like it to be formatted (without a mustUnderstands value in it). The rest of the code is for passing along the userName and password so that it can be placed into the Security header.

The MessageBehavior needs to be assigned to the WCF client. This is done using the following code:

MessageBehavior messageBehavior = new MessageBehavior("username", "password");
client.Endpoint.Behaviors.Add(messageBehavior);

Where client is an instance of the WCF client.

Finally, and this ended up being a key factor that I had to learn after a lot of trial and error, is that I needed to modify the bindings used by WCF to use basicHttpBinding with just transport level security (https). The reason for this is that if use the TransportWithMessageCredential, then even though you might be adding the security headers in the BeforeSendRequest method, WCF will continue adding its version of the Security headers (and these contain the mustUnderstand set to true and will cause the server to fault). With the correct bindings and the MessageInspector in place…. everything began working.

Here is what the updated bindings looked like:

<basicHttpBinding>
      <binding name="basicBinding">
            <security mode="Transport">
                 <transport clientCredentialType="None" proxyCredentialType="None"
                            realm="" />
                 <message clientCredentialType="UserName" algorithmSuite="Default" />
            </security>
      </binding>
</basicHttpBinding>

Note 1:

Instead of using the pre-defined basicHttpBinding you can use the following custom binding.

<customBinding>
     <binding name="basicHttpTransportSecurityUserNameMessage">
          <textMessageEncoding messageVersion="Soap11"/>
          <httpsTransport />
     </binding>
</customBinding>

Note 2:

Like I mentioned before, even though this is a solution around the mustUnderstand issue when using UserNameToken over https with Soap11 (basicHttpBinding), I dont like the fact that I am inserting my own header XML into the message. The main reason is that this solution might need a lot of testing and learning to get it working for your problems. If you find that this solution does not work for you, the first thing I would suggest is to use SoapUI and determine what the headers being sent look like, when you perform a successful request to the web-service. Compare this header with the one in my BeforeSendRequest method. Try and replace the header text to see if this makes your client work correctly. Another extremely useful tool was Fiddler, which allowed me to monitor the data being sent by the WCF client.

Note 3:

Custom Message Encoders might be another solution for suppressing the mustUnderstand attribute. I haven’t tried this yet. But it looks more flexible, although it is also the more complex solution too.

CustomMessageEncoder sample: http://msdn.microsoft.com/en-us/library/ms751486.aspx

Note 4:

Another issue that you might face is when the certificate being used by the server for the HTTPS connection is not valid for some reason (most common of which is the name of the server does not match the CN name in the certificate). In such cases, you need to call the following code before any call to WCF.

ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate{return true;});

More Information:

Message Inspectors: http://msdn.microsoft.com/en-us/library/aa717047.aspx

WCF Security and Silverlight 2: http://www.netfxharmonics.com/2008/11/Understanding-WCF-Services-in-Silverlight-2#WCFSilverlightIntroduction

5 comments:

Unknown said...

It may be "hacky" as you put it, but I'm glad you posted it. It is helping me with a related issue, that WCF doesn't support hashed password digests, whereas the old Microsoft Web Service Extensions did.

I'm getting the error "no SOAPAction header!". Have you seen that before?

Looking at the message trace, the Action does appear to be blank:




https://website.com/operation


Ping me on Twitter if you want to make contact!

Jack said...

Hi Raj,

I could find a part of this article useful to resolve the issue I had for passing the security header. Thanks a lot!!

- Mukesh Tank

Robbie said...

Thank you, thank you, thank you! This really helped.

Robbie said...

Thank you, thank you, thank you! After extensive searching, this was the answer to my problem.

Phantom2018 said...

Obviously it has been several years this post has been alive - but the fact is I did find it when looking for a similar issue. In our case, we had to add the username / password info to the Security header. This is different from adding header info outside of the Security headers.
The correct way to do this (for custom bindings / authenticationMode="CertificateOverTransport") (as on the .Net framework version 4.6.1), is to add the Client Credentials as usual :
client.ClientCredentials.UserName.UserName = "[username]";
client.ClientCredentials.UserName.Password = "[password]";

and then add a "token" in the security binding elemen - as the username / pwd credentials would not be included by defauls when the authentication mode is set to certificate.

You can set this token like so:

//Get the current binding
System.ServiceModel.Channels.Binding binding = client.Endpoint.Binding;

//Get the binding elements
BindingElementCollection elements = binding.CreateBindingElements();

//Locate the Security binding element
SecurityBindingElement security = elements.Find();

//This should not be null - as we are using Certificate authentication anyway
if (security != null)
{
UserNameSecurityTokenParameters uTokenParams = new UserNameSecurityTokenParameters();
uTokenParams.InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient;
security.EndpointSupportingTokenParameters.SignedEncrypted.Add(uTokenParams);
}

That should do it. Without the above code (to explicitly add the username token), even setting the username info in the client credentials may not result in those credentials passed to the Service.