3
votes

I have a WCF service (.NET 4) hosted in IIS (7.5 on Windows Server 2008 R2). Our customer (for whom we are building the service) requires that all clients of the service be authenticated using certificates from Verisign. That is, the client purchases a certificate from Verisign and provides us with the public key. The service should only accept requests that can be validated using one of the public keys that we have received from the clients.

The problem that I am having is that IIS seems to require that Anonymous authentication be enabled. If it isn't, a "Security settings for this service require 'Anonymous' Authentication but it is not enabled for the IIS application that hosts this service." error is raised. However, if I enable anonymous authentication, any call to the service that provides any certificate is allowed. We need to restrict it to only allow calls that provide specific certificates (i.e. ones for which we have been supplied the public key).

In IIS we require SSL and client certificates (under SSL Settings), and (under Authentication) all authentication is disabled (except for anonymous).

The web.config is:

  <system.web>
    <compilation debug="false" targetFramework="4.0" />
    <authentication mode="Windows" />
    <authorization>
      <deny users="?" />
      <allow users="*" />
    </authorization>
    <customErrors mode="Off" />
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" />
    <identity impersonate="false" />
  </system.web>

  <system.serviceModel>
    <services>
      <service behaviorConfiguration="XXXServiceBehavior" name="XXXService.NewService">
        <endpoint address=""
                  binding="wsHttpBinding"
                  bindingConfiguration="XXXServiceBinding"
                  contract="XXXService.INewService"
                  bindingNamespace="http://XXXX.com.au/XXXService/NewService">
        </endpoint>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="XXXXServiceBehavior">
          <serviceDebug includeExceptionDetailInFaults="true" />
          <serviceCredentials>
            <clientCertificate>
              <authentication certificateValidationMode="PeerTrust" mapClientCertificateToWindowsAccount="true" />
            </clientCertificate>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <bindings>
      <wsHttpBinding>
        <binding name="XXXServiceBinding" maxReceivedMessageSize="1000000" maxBufferPoolSize="1000000">
          <readerQuotas maxStringContentLength="1000000" />
          <security mode="Transport">
            <transport clientCredentialType="Certificate" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
  </system.serviceModel>

If anyone is able to get this sort of scenario working, could you please let me know what settings you are using in IIS and in your configuration?

1
Which Windows accounts are the certificates being mapped to? Could you use those in conjunction with PrincipalPermissionAttribute to demand either Windows auth or membership of a particular group?anton.burger
I did have a quick go at that, but couldn't get it working. I'll have another go at it today. However, it seems to me that there should be a much simpler way of doing this.MarkShep
OK, I got it working this time, and it ultimately does what I need, though I don't think it's an elegant solution. I couldn't use an attribute, I had to demand the permission in code (because the PrincipalPolicy has to be set to WindowsPrincipal, which seems to need to be done in code). That means that some code is being executed before determining that the user shouldn't be allowed to get in at all. Also, this is just one more area where human error could leave a gaping hole in security. So I don't like it, but it's the most useful idea I've found so far, so thanks.MarkShep
Why does it seem to have to be done in code? Does adding serviceAuthorization and specifying Windows groups to your serviceBehavior help?anton.burger
Yes, I needed <serviceAuthorization principalPermissionMode="UseWindowsGroups" /> in the configuration. Although it turns out that what I was really looking for all along was to use TransportWithMessageCredential, but I'll post the details as an Answer.MarkShep

1 Answers

1
votes

It turns out that what I needed was to use a binding security mode of TransportWithMessageCredential, and a message clientCredentialType of Certificate (see the full config listing below).

With this binding configuration I don't need to demand the principal permissions or any mapping of certificates to Windows accounts, since the service won't get executed if the caller does not provide a recognized certificate (i.e. a certificate whose public key has been imported into the 'Trusted People' store on the server, at the machine level).

The only other setting change is in IIS and the SSL Settings. I had to change the 'Client certificate' setting from 'Require' to 'Accept'. (Leaving it as 'Require' results in a "The HTTP request was forbidden with client authentication scheme 'Anonymous'" error being received by the client.) I'm not sure why this is the case, but I don't think this is a problem since WCF will require the certificate.

So here's the relevant config. This is the server config:

<system.web>
  <compilation debug="false" targetFramework="4.0" />
  <authentication mode="Windows" />
  <authorization>
    <deny users="?" />
    <allow users="*" />
  </authorization>
  <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" />
  <identity impersonate="false" />
</system.web>

<system.serviceModel>
  <services>
    <service behaviorConfiguration="XXXWebServiceBehavior" name="XXXWebService.NewService">
      <endpoint address="" binding="wsHttpBinding" bindingConfiguration="XXXWebServiceBinding" contract="XXXWebService.INewService" />
    </service>
  </services>
  <behaviors>
    <serviceBehaviors>
      <behavior name="XXXWebServiceBehavior">
        <serviceDebug includeExceptionDetailInFaults="false" />
        <serviceCredentials>
          <clientCertificate>
            <authentication certificateValidationMode="PeerTrust" />
          </clientCertificate>
        </serviceCredentials>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <bindings>
    <wsHttpBinding>
      <binding name="XXXWebServiceBinding" maxReceivedMessageSize="1000000" maxBufferPoolSize="1000000">
        <readerQuotas maxStringContentLength="1000000" />
        <security mode="TransportWithMessageCredential">
          <message clientCredentialType="Certificate" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>
</system.serviceModel>

And here's the client config (mostly generated by Visual Studio):

<system.serviceModel>
  <bindings>
    <wsHttpBinding>
      <binding name="WSHttpBinding_INewService" closeTimeout="00:01:00"
               openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
               bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
               maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
               messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
               allowCookies="false">
        <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                      maxBytesPerRead="4096" maxNameTableCharCount="16384" />
        <reliableSession ordered="true" inactivityTimeout="00:10:00"
                         enabled="false" />
        <security mode="TransportWithMessageCredential">
          <message clientCredentialType="Certificate" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>
  <client>
    <endpoint address="https://XXXX.XXXX.com.au/XXXServices/NewService.svc"
              binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_INewService"
              contract="ServiceReference1.INewService" name="WSHttpBinding_INewService">
      <identity>
        <dns value="localhost" />
      </identity>
    </endpoint>
  </client>
</system.serviceModel>