Pages

Sunday, March 13, 2016

Breaking All The Rules with WCF Full Circle


Recently while working on a project for a large government enterprise, I encountered a problem similar, nay, exactly the same as Scott Hanselman in his post "Breaking All The Rules with WCF." From the malformed WSDL to a custom WS-Security usernameToken without a password, I was dealing with the same issue.

Scott, as always, has done an incredible job highlighting the problem, and providing a solution, so I highly encourage you to read his post. I'm writing this post to cover the one thing he missed, creating a service that can be used to test this binding configuration against.

CREATING THE BINDING

Scott was able to create a Custom WCF binding that emits a username without password using the following code:

WSHttpBinding oldBinding = new WSHttpBinding();
oldBinding.Security.Mode = SecurityMode.TransportWithMessageCredential;
//Just the username
oldBinding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
//And basically nothing else
oldBinding.Security.Message.NegotiateServiceCredential = false;
oldBinding.Security.Message.EstablishSecurityContext = false;

//oldBinding.ProxyAddress = new Uri("http://BIGASSLAPTOP:8888");
//oldBinding.UseDefaultWebProxy = false;

//remove the timestamp
BindingElementCollection elements = oldBinding.CreateBindingElements();
elements.Find().IncludeTimestamp = false;

//sets the content type to application/soap+xml
elements.Find().MessageVersion = MessageVersion.Soap12;
CustomBinding newBinding = new CustomBinding(elements);
FooPortTypeClient svc = new FooPortTypeClient(newBinding, new EndpointAddress("https://example.com/foo/v1"));
FooRequest req = new FooRequest();
//...etc...now it's just request and response.

The same binding can be created in configuration like this:

<bindings>
 <customBinding>
  <binding name="WSSecurityBinding">
  <security defaultAlgorithmSuite="Default"
     authenticationMode="UserNameOverTransport"
     requireDerivedKeys="true"
     includeTimestamp="false"
     messageSecurityVersion="WSSecurity11WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
   <localClientSettings detectReplays="false" />
   <localServiceSettings detectReplays="false" />
  </security>
  <textMessageEncoding messageVersion="Soap12" />
  <httpsTransport />
  </binding>
 </customBinding>
</bindings>

This will create the binding, but what about testing it?

TESTING

If you configure a test client and service with this binding you'll find that as soon as the client calls the service, you'll receive the following exception:


This exception is thrown because the default UserNamePasswordValidator expects both a username and a password... and we just removed the password. However, fixing this issue is easy enough... we can create a custom UserNamePasswordValidator that allows us to validate the request according to our custom business logic.

public class NullPasswordValidator : UserNamePasswordValidator
{
    public override void Validate(string userName, string password)
    {
        if (string.IsNullOrWhiteSpace(userName))
            throw new FaultException("The userName cannot be null or whitespace.");

        // Allow any password, even null values to pass.
    }
}

Configuring the service to use this new validator is as simple as creating a new service behavior configuration and pointing to the new custom validator:

  <serviceBehaviors>
    <behavior name="NullPasswordValidator">
      <serviceCredentials>
        <userNameAuthentication userNamePasswordValidationMode="Custom"
                                customUserNamePasswordValidatorType="Security.WCF.Validators.NullPasswordValidator, Security" />
      </serviceCredentials>
    </behavior>
  </serviceBehaviors>

FULL CIRCLE

After creating and configuring the new UserNamePasswordValidator, our client will be able to communicate with the service. We can open up Fiddler and prove that everything is working correctly by inspecting the SOAP message:

<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">
 <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">
   <o:UsernameToken u:Id="uuid-38dc2ff9-a055-4a66-90e3-3aa0c267c36e-1">
    <o:Username>viaMacchina\\dec</o:Username>
   </o:UsernameToken>
  </o:Security>
 </s:Header>
 <s:Body>
  ...
 </s:Body>
</s:Envelope>

This brings the custom binding full circle. We're able to create a message request without a password to fulfill the custom WS-Security requirements. We're also able to create a test service that will accept the requests without throwing an exception. Finally, we can use tools like Fiddler to inspect the SOAP messages between the client and service to ensure that the message is formed exactly as it should be.

No comments:

Post a Comment