Change the way you define your identity with .NET 3.5 SP1
A service’s endpoint identity is an important entity in an authentication process. Any WCF developer should have a good understanding of what it is, how it is set at the service and client and what part it plays in the overall authentication process. For those in doubt, MSDN article on ‘ Service Identity and Authentication ’ will surely make a lot of those things clear.
Today I will concentrate my discussion on how a service’s endpoint identity can be set within a client application. Most common approach will be to configure it within a client configuration file enclosed within a <identity> element. For the sake of the topic, I will consider the following binding:
<wsHttpBinding>
<binding name="Message_Windows">
<security mode ="Message">
<message clientCredentialType="Windows"
establishSecurityContext="false"
negotiateServiceCredential="false"
algorithmSuite="Basic128"/>
</security>
</binding>
</wsHttpBinding>
Let’s say that my service is hosted on IIS and the application pool is running under a custom service account. I have a HTTP SPN associated with that service account. Check this blog for more insights into SPN.
Account name: somedomain\tyler
Configured SPN: HTTP/tyler.durden.com
Going back to how we can provide this information inside a client configuration file:
<client>
<endpoint address="https://somedomain.com/SimpleKerberosTest/Service.svc"
binding="wsHttpBinding" bindingConfiguration="Message_Windows"
contract="ServiceReference.IService" name="WSHttpBinding_IService">
<identity>
<servicePrincipalName value="HTTP/tyler.durden.com" />
</identity>
</endpoint>
</client>
All we need now is to instantiate a service proxy within our client application passing to it this endpoint configuration name.
ServiceClient proxy = new ServiceClient("WSHttpBinding_IService");
Console.WriteLine(proxy.GetData(10));
Sometimes there is a necessity for us to pass the endpoint address dynamically via the code. That can easily be accomplished by using any of System.ServiceModel.ClientBase<T> constructors which accept a EndpointAddress object as one of its parameters.
ServiceClient proxy = new ServiceClient("WSHttpBinding_IService", <EndpointAddress object> or remote endpoint as a string input);
This whole scenario works like a charm in .NET 3.5. With the application of SP1, the same scenario fails with the following exception message:
<ExceptionType>System.ServiceModel.Security.MessageSecurityException, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType>
<Message>The token provider cannot get tokens for target 'https://somedomain.com/SimpleKerberosTest/Service.svc'. </Message>
Inner Exception Details:
<ExceptionString>System.ComponentModel.Win32Exception: The specified target is unknown or unreachable</ExceptionString>
<NativeErrorCode>80090303</NativeErrorCode>
How does the same environment now start complaining about an unreachable or unknown target? Collect WCF trace for the error and have a look at the determined identity (collect verbose tracing and look for ‘Identity was determined for an EndpointReference’):
<Identity xmlns="**https://schemas.xmlsoap.org/ws/2006/02/addressingidentity**"\>
<Dns> somedomain.com </Dns>
</Identity>
<EndpointReference xmlns="**https://www.w3.org/2005/08/addressing**"\>
<Address> **https://somedomain.com/SimpleKerberosTest/Service.svc**\</Address>
</EndpointReference>
To our surprise what we see is a DNS generated by the framework. It then appends ‘HOST’ to this value to generate a SPN identity (HOST/somedomain.com). Eventually a HOST SPN is submitted by the client to active directory in request for a service ticket. In my test environment, I do not have any HOST SPNs registered. Hence the error message ‘target is unknown or unreachable’. Usually a HOST SPN will be associated with a computer account. In that scenario, the same request will fail with either of the following error messages:
The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate oYG7MIG4oAMKAQGigbAEga1ggaoGCSqGSIb3EgECAgMAfoGaMIGXoAMCAQWhAwIBHqQRGA8yMDA4MDgxNDEwMzk0OVqlBQIDBlSwpgMCASmpJRsjUEFSVFRFU1QuRVhUUkFORVRURVNULk1JQ1JPU09GVC5DT02qRTBDoAMCAQOhPDA6GwRob3N0GzJuZ2ltdHdpcGNybWFwcC5wYXJ0dGVzdC5leHRyYW5ldHRlc3QubWljcm9zb2Z0LmNvbQ
OR
The remote server returned an error: (401) Unauthorized
This proves that with the application of SP1, we do not read the identity element from the client configuration file. This is a DESIGN CHANGE implemented with SP1. However one will encounter this issue only when using a ClientBase<T> which accepts a remote address as one of its parameters.
Good news is that we have a couple very simple workarounds to resolve this issue:
1. Use ChannelFactory to create a proxy.
IService proxy = null;
ChannelFactory<IService> factory = null;
EndpointIdentity identity = EndpointIdentity.CreateSpnIdentity("HTTP/tyler.durden.com");
EndpointAddress address = new EndpointAddress(new Uri("https://somedomain.com/SimpleKerberosTest/Service.svc"), identity);
factory = new ChannelFactory<IService>("WSHttpBinding_IService", address);
proxy = factory.CreateChannel();
Console.WriteLine(proxy.GetData(10));
In the above scenario, the only limitation is hard coding of identity inside the code. That can easily be altered by reading the value from a configuration file.
2. Use a dummy proxy to read the identity value from configuration file and use it to create another proxy object.
ServiceClient proxy = new ServiceClient("WSHttpBinding_IService");
EndpointIdentity identity = proxy.Endpoint.Address.Identity;
EndpointAddress address = new EndpointAddress(new Uri("https://somedomain.com/SimpleKerberosTest/Service.svc "), identity);
ServiceClient newProxy = new ServiceClient("WSHttpBinding_IService", address);
Console.WriteLine(proxy.GetData(10));
This way we can read the identity element from configuration file as well as set endpoint address dynamically via code.
Either ways will result in the client using correct SPN to submit to active directory. Now if you look into WCF trace one more time, you will see correct SPN:
<EndpointReference xmlns="**https://www.w3.org/2005/08/addressing**"\>
<Address> https://somedomain.com/SimpleKerberosTest/Service.svc </Address>
<Identity xmlns="**https://schemas.xmlsoap.org/ws/2006/02/addressingidentity**"\>
<Spn>HTTP/tyler.durden.com</Spn>
</Identity>
</EndpointReference>
While it was pretty simple for me to figure out why was the request failing (due to incorrect SPN), but it took me quite a while to figure out that it was a design change implemented by SP1. Hope this will save others a lot of precious time.