Tuesday, 14 September 2010

Exception when using WCF Client to Web Service Reference behind a proxy

Are you getting the following exception when calling a simple ASMX service via a WCF service reference from behind a firewall (in this case ISA Server): 

"Server encountered an internal error. For more information, turn off customErrors in the server's .config file."

You'd expect there to be an inner exception, but there isn't.  You can get to the actual exception by enabling all exceptions to cause a breakpoint in VS2005/2008 (Menu: Debug->Exceptions and then click the checkbox at the root of all .Net exceptions - be careful doing this in a web site, though, because a tonne of exceptions get thrown and swallowed by the Asp.Net engine!).

The actual error that is occuring is a WebException: [407] Proxy Authentication Required.

In my case, I've also been using Fiddler to debug the SOAP that's going to and from (if you haven't got it, then get it now!) my machine, and had also noticed that when it was running, the exception would go away.  This is probably because of some behaviour on ISA Server where it will reuse existing credentials for a connection to a particular outgoing domain until a certain expiry time is reached - did throw me a curve however, until I figured that out!

Quick note - if you're looking at writing stuff to leverage TFS via it's web services and want to know how the Team Explorer and the Source Control Explorer do the things they do - it's a good idea to use Fiddler to examine the SOAP.

The problem here is an age old one where you have a proxy (in this case ISA Server) through which all your outgoing network traffic is performed.  In my environment, the ISA Server requires credentials for a user on the domain that is allowed through the proxy.  Without it, the connection will be refused, causing a RemotingException.

Now, in the past this could be solved using the System.Net.CredentialCache.DefaultNetworkCredentials property, and assigning it to a property of the SOAP client proxy that is generated - done.  At first glance, there appears to be nothing as simple as that on the ClientBase<T>-deriving type that the WCF service reference generator in VS creates for you.

Looking closer, however, we see the member ClientBase<T>.ClientCredentials, which has a 'Windows' property, which in turn has a 'ClientCredential' property of type NetworkCredentials.  Thinking that this would solve my problem, I wrote the following code:

  1. MyWebServiceClient client = new MyWebServiceClient();
  2. client.ClientCredentials.Windows.ClientCredential =
  3.     System.Net.CredentialCache.DefaultNetworkCredentials;

I should mention at this point, that the project I'm working on doesn't bind it's web service references using the WCF config files - because I have to bind to a dynamic endpoint.  Anybody working with the configs, however, should still be able to use this post to correct this problem when using the config files. 

Anyway, the exception still gets raised. 

I then tried manually filling out the user name, password and domain, but that didn't work either [bangs head on wall].

Found this MSDN forum where the guy solved his problem using config.  not a great use for me, given that I'm configuring declaratively but there is one nugget in there - where the guy sets the HttpTransportBindingElement.UseDefaultWebProxy property to false, and then specifies the address of the proxy in the HttpTransportBindingElement.ProxyAddress property.

This involved also using the CustomBinding class - which I don't want to use.  I simply want to create a BasicHttpBinding object, set some properties, and have done with it.

So I tried switching off the default proxy behaviour, and then give WCF the address of the default proxy(!) - read it again if you can't see why I think this is ludicrous.

Here's my eventual code:

  1. [TestMethod]
  2. public void TestMethod1()
  3. {
  4.     //create a binding
  5.     BasicHttpBinding binding = new BasicHttpBinding
  6.         (BasicHttpSecurityMode.TransportCredentialOnly);
  7.     binding.Security.Transport.ClientCredentialType =
  8.         HttpClientCredentialType.Ntlm;
  9.     binding.Security.Transport.ProxyCredentialType =
  10.         HttpProxyCredentialType.Ntlm;
  11.     //don't read the system proxy
  12.     binding.UseDefaultWebProxy = false;
  13.     //in my case - the URL I placed here was the same
  14.     //as the one assigned by group policy! (WTF?)
  15.     binding.ProxyAddress =
  16.         new Uri("http://proxy:portnum");
  17.  
  18.     //configure the endpoint address
  19.     EndPointAddress addr =
  20.         new EndPointAddress("http://domain/service.asmx");
  21.  
  22.     //create an instance of the client type generated by the add
  23.     //service reference tool using the binding and the endpoint
  24.     //address that we've configured manually
  25.     MyWebServiceClient c = new MyWebServiceClient(binding, addr);
  26.  
  27.     //make sure that the network credentials are set on the client.
  28.     ClientCredentials creds =
  29.         c.ClientCredentials;
  30.     creds.Windows.ClientCredential =
  31.         CredentialCache.DefaultNetworkCredentials;
  32.     //should bee true by default - but be safe.
  33.     creds.Windows.AllowNtlm = true;
  34.     //if the web service at the other end needs to use these credentials
  35.     //for impersonation or whatever, then this level should be set
  36.     //accordingly.
  37.     creds.Windows.AllowedImpersonationLevel =
  38.         System.Security.Principal.TokenImpersonationLevel.Identification;
  39.  
  40.     //now call the method
  41.     MyWebServiceClient.Foo();
  42. }

And it works fine.

A quick note on this.  I am a great fan of MSDN - typically my first port of call is the F1 help, go to the index, type the class into that, and then browse through.  Typically you get some choice examples, with some extended documentation on the impact of different values of various settings etc.  In some parts of the framework, you even find links out to high-level conceptual topics to see 'where you are' in the overall process of whatever that framework is trying to achieve. 

Not with WCF - the F1 help for all the WCF classes is shoddy.  It has to be the least well-documented significant framework since SMO (don't even get me started on that) - not joined up in any way.

Anyway, I hope this post helps somebody out!

No comments:

Post a Comment