WCF: IIS Hosting, Windows Authentication and File Permissions

July 25th 2012 WCF IIS Administration

Troubleshooting WCF services is often challenging, mostly because of cryptic and uninformative error messages. When you combine that with complex usage and configuration scenarios, it takes time to get to the bottom of the problem. The issue I'm about to describe manifested itself on a production server running a WCF service with HttpModule based Windows authentication. Other authentication methods worked fine, while with Windows authentication WCF returned a rather cryptic error message:

The HTTP request is unauthorized with client authentication scheme 'Negotiate'. The authentication header received from the server was 'Negotiate,NTLM'.

Since the exact same error message is being returned to the client when the HttpModule replaces the WindowsPrincipal in the current HttpContext with a custom IPrincipal, my first thought was that for some reason HttpModule was misbehaving. Adding detailed tracing to the HttpModule proved that this was not the case: WindowsPrincipal was kept intact. Now that my only potential cause for the problems turned out not to be at fault, it was time to localize the problem by simplifying the test case.

That meant trying out a simple WCF service using plain Windows authentication without a HttpModule. I always have one handy for cases like this:

[ServiceContract]
public interface IService1
{
    [OperationContract]
    string GetData(int value);
}

public class Service1 : IService1
{
    public string GetData(int value)
    {
        return String.Format("You entered: {0}\nUsername: {1}", value, 
            ServiceSecurityContext.Current == null ? 
                "<null>" : 
                ServiceSecurityContext.Current.PrimaryIdentity.Name);
    }
}

You might recognize it as a slightly modified WCF service template included in Visual Studio. Of course web.config file needs to be modified as well:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <authentication mode="Windows"/>
  </system.web>
  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <basicHttpBinding>
        <binding name="HttpWindowsBinding" maxReceivedMessageSize="2147483647">
          <readerQuotas maxBytesPerRead="2147483647" maxArrayLength="2147483647" 
                        maxStringContentLength="2147483647" 
                        maxNameTableCharCount="2147483647" maxDepth="2147483647"/>
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
      </basicHttpBinding>
    </bindings>
    <services>
      <service name="TestService.Service1" behaviorConfiguration="ServiceBehavior">
        <endpoint address=""
                  binding="basicHttpBinding"
                  bindingConfiguration="HttpWindowsBinding"
                  contract="TestService.IService1" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

The client app just references the service and calls its only method:

class Program
{
    static void Main(string[] args)
    {
        try
        {
            var proxy = new ServiceReference1.Service1Client();
            Console.Write(proxy.GetData(1));
        }
        catch (Exception e)
        {
            Console.Write(e.ToString());
        }
        Console.ReadLine();
    }
}

For the sake of completeness, here's the app.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="HttpWindowsBinding" 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="TransportCredentialOnly">
                        <transport clientCredentialType="Windows" 
                                   proxyCredentialType="None" realm="" />
                        <message clientCredentialType="UserName" 
                                 algorithmSuite="Default" />
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost/TestService/Service1.svc"
                      binding="basicHttpBinding" 
                      bindingConfiguration="HttpWindowsBinding"
                      contract="ServiceReference1.IService1" />
        </client>
    </system.serviceModel>
</configuration>

Playing with this test service provided the following results:

  • Even in this simple case the same error manifested itself when the client was running under a standard user account.
  • When the client was running under an administrative account, the service worked just fine.

It didn't matter whether the client application was running on the server or on a separate client machine - the results were the same. This ruled out a bug in the application code. The problem was being caused by a configuration issue: it had something to do with permissions, file permission to be exact.

After looking at IIS application configuration it turned out pass-through authentication was being used for path credentials:

Path credentials

The file permissions on the folder with service files were of course very restrictive: giving access only to administrative users. The combination of these two settings caused the weird error when a non-administrative user was calling the service. The first request was unauthenticated and therefore accessed the files with the application pool credentials. Once it was denied the client submitted its Windows credentials which could now be used for accessing the files as well. Since suddenly the files couldn't be accessed any more, WCF runtime returned the before mentioned error.

Fixing the problem was an easy job now. Any one of the following two solutions would work:

  • My preferred approach is to set a specific administrative user for path credentials. This allows keeping tight security on the files in the file system.
  • Alternatively file permissions can be given to all the users accessing the service: either by allowing access to all authenticated users or by limiting it to a specific group containing all service users. In the latter case adding new users would also mean they would have to be added to this group as well.

Problem solved. The error could still be more descriptive, though.

Copyright
Creative Commons License