Tuesday, 22 October 2013

Finally - Azure with client certificates!

Introduction

The basic setup is that I have a web application which is public and a web service which is private. Since they are both hosted on Azure, I thought I would use client certificates as a way to ensure only the web application can access the web service. I had untold problems with it and got fed up with the odd IIS type errors about mismatches and authentication modes so I kind of gave up and hosted the web service on an Azure virtual machine so I could configure it all.

Anyway, there were a few problems with the raw virtual machine approach. It is harder to scale up (although I think it is possible with Azure VMs), it costs slightly more to run and although it was quick to deploy using SubVersion directly onto the box, I had so many problems trying to update the web site without it causing the web application to stop communicating with it properly. I decided enough was enough and tried to set up the client certificates in a cloud service again.

Client Certificates

You probably already know what these are if you are reading this article but if you don't, the basic idea is to use an X509 (SSL/TLS) type certificate to prove to the server that you are an authorised client. Using this mechanism is a good way of protecting your public facing but private services/applications from prying eyes. Note, that although you can possibly use these without https, I would not recommend it.

You can self-sign certificates but that opens a can of worms regarding how to check whether the certificate is genuine and having to override various errors but in my case, I am using SSL certificates from a root authority, which are pretty cheap really for the extra security.

Server End

Code

The web service is pretty much a standard WCF web service but you have to be a little careful with namespaces and stuff (that is not related to client certificates but I might as well include it all to make sure it works). I HIGHLY recommend testing the basic web service without too much security just to make sure it works. The security settings can drive you mad but your problem might be related to something much simpler.

So... the service interface itself looks like this:

namespace com.mycompany.WebServices
{
    [ServiceContract(Namespace = "http://mycompany.co.uk/com.mycompany.WebServices.CC")]
    public interface IMyWebService
    { //etc...

Naturally, the exact namespaces are not too important (and don't need to use https in the name) but best to keep the standard format. The implementation is this:

namespace com.mycompany.WebServices
{
    [ServiceBehavior(Namespace = "http://mycompany.co.uk/com.mycompany.WebServices.CC")]
    public class MyWebService : System.Web.Services.WebService, IMyWebService
    { //etc...

This is pretty standard stuff and although you can often live without the namespaces, it can make it really difficult if you start referencing more than one web service from your client with type conflicts etc.

Configuration

There are obviously lots of little bits, many of these are specific to my implementation (buffer sizes etc) but again, I will include it all so that you get an example that definitely works. The configuration for the service looks like this:

<system.serviceModel>
    <services>
      <service behaviorConfiguration="defaultBinding" name="com.mycompany.WebServices.MyWebService">
        <endpoint address="standard" binding="wsHttpBinding" bindingConfiguration="standardConfig"
          name="standard" bindingName="standard" bindingNamespace="http://mycompany.co.uk/com.mycompany.WebServices.CC"
          contract="com.mycompany.WebServices.IMyWebService" />
     </service>
    </services>
    <bindings>
    <customBinding>
        <binding name="wsdlBinding">
          <textMessageEncoding messageVersion="None" />
          <httpsTransport requireClientCertificate="true" />
        </binding>
      </customBinding>
      <wsHttpBinding>
        <binding name="standardConfig" maxBufferPoolSize="10485760" maxReceivedMessageSize="10485760">
          <readerQuotas maxDepth="32" maxStringContentLength="10485760" maxArrayLength="10485760" maxBytesPerRead="10485760" maxNameTableCharCount="10485760" />
          <security mode="Transport">
            <transport clientCredentialType="Certificate" />
            <message clientCredentialType="None" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="defaultBinding">
          <serviceMetadata httpGetEnabled="false" httpsGetEnabled="true" httpsGetBinding="customBinding" httpsGetBindingConfiguration="wsdlBinding"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
          <serviceCredentials>
            <clientCertificate>
              <certificate findValue="6FE3C33463GF87GFF94DC52479E72BFF60A1C34B" x509FindType="FindByThumbprint" storeLocation="LocalMachine" storeName="My"/>
            </clientCertificate>
            <serviceCertificate findValue="AB656DEE53E920801D23A5CC90250CB11F88D62E" storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint"/>
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>
  </system.serviceModel>


There are not too many things to worry about here but note that I have a service certificate for the https and another certificate that is what the clients use to connect and authenticate. Both of these are added into the Azure configuration as per normal certificates (both of them in LocalMachine/My). I have also included the certificate for my certificate provider to provide the full trust chain and this is also added in LocalMachine/My. I have not needed to do this for the root certificate which is obviously included on Azure. All of these certificates need to be uploaded to the Azure management interface so they are available to the project during deployment.

I then have to unlock the access section of the web config to force the use of SSL and require a client certificate. You can create a startup file like this: http://msdn.microsoft.com/en-us/library/windowsazure/gg456327.aspx and inside that file, you need to run "%windir%\System32\inetsrv\appcmd.exe unlock config /section:system.webServer/security/access" without the quotes.

Then inside your web config, add the following section inside system.webServer:

<security>
   <access sslFlags="Ssl,SslNegotiateCert"/>
</security>

Client End

The client needs some similar configuration but note that using "Add Service Reference" inside visual studio did not work as expected, so I used SvcUtil.exe (which is part of the Windows SDK if you need to download it) and running SvcUtil.exe against the wsdl like this:

SvcUtil.exe https://mycompany.co.uk/MyWebService.svc?wsdl

produced the relevant client code (a .cs file) and an example configuration to use for the client. I already had the client configuration setup so I didn't use this.

I had to include the .cs file from SvcUtil into my project and then create the client configuration in my web config in much the same way as it looks in the web service end:

<system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="standard">
          <security mode="Transport">
            <transport clientCredentialType="Certificate" />
            <message clientCredentialType="None" />
          </security>
        </binding>
      </wsHttpBinding>
      <webHttpBinding>
        <binding name="webHttpBinding" />
      </webHttpBinding>
    </bindings>
    <client>
      <endpoint address="https://mycompany.co.uk/MyWebService.svc/standard"
        behaviorConfiguration="CCBehaviour" binding="wsHttpBinding" 
        bindingConfiguration="standard" contract="IMyWebService" name="standard" />
    </client>
    <behaviors>
      <endpointBehaviors>
        <behavior name="CCBehaviour">
          <clientCredentials>
            <clientCertificate findValue="6FE3C33463GF87GFF94DC52479E72BFF60A1C34B" storeLocation="LocalMachine" storeName="My" x509FindType="FindByThumbprint" />
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>


As before,the certificate referenced in the clientCertificate element was added to the client's Azure configuration in the LocalMachine/My store and also uploaded to the Azure control panel.

Conclusion

It seemed easy once I had it all working but I'm not sure where I got it wrong before. I'm always suspicious of any caching mechanisms or the classic of mine which is, "break something just before you fix something" that just keeps pushing the problem to somewhere else! Hopefully this will help you get started with the client certificates functionality, a great way to secure communications with a web service.
Post a Comment