Saturday, September 10, 2011

Consume a WCF service that uses Federated Security

This post is not about Active Directory Federated Security, but it is about using a custom Security Token Service (STS) to create a token.   I needed to connect to a third party web service that used Federated Security. Now they supported ADFS but I only needed to access using a single account and I really didn't want to set up ADFS just for that.  But I could use a custom STS service to create the token and avoid all the infrastruture overhead of ADFS.

Setup begins with the third party sending me the url to their STS and a copy of the public key certificate they will use to sign their token. I also need to have the url to their web service I want to call.

I'll say right away I'm no expert on this. There are examples in the Windows Identity SDK and that is a good starting point. The way it would work is my client application (a  Windows Service) would call a local STS and pass it a username and password.  My STS would check the credentials and if OK issue a token. Now the token is signed and encrypted by my X509 certificate so I have to get one of those to begin with.  I need to install my X509 certificate in the Personal  store of  the computer I run the code on.
Start MMC, add the Certficate add-in and select the Local Computer. Navigate to the Personal node, and Certificates, right click and import my X509 certificate.  Since I am running my Windows Service under the Network Service account I need to assign permissions.  On the Certificate, right click, select All Tasks then Manage Private key. On the Security tab, add   Network Service and give it full permissions. I can also add the thrid party's certificate here too - it is imported into the Personal Certificate store in the same way but since they gave me the public key, I don't need to bother with the permissions step (its not available anyway).

I also need to send the public key of my certificate to the third party.  Right click on the certificate, select Export and choose the option to export the public key only. 

Now the token issued by my STS contains a claim, in my case it is simply the role of Reader.  I then pass my token to the third party's STS. These guys will want to check the token is from me and they use the certificate I sent them to do so.  They check the claim, and if all is well issue me a token from their STS. 

I suppose I should validate their token with the certificate they sent me, but I'm not going to bother as I'm going to send it straight back to them when I call their web service. 
When I look at the WSDL of their STS service I can see it is different from a typical WCF service because it has a section at the bottom which looks like this.

<identity xmlns="http://schemas.xmlsoap.org/ws/2006/02/addressingidentity">
<keyinfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<x509data>
<x509certificate>MIIE8zCCA9ugAwIBAgILAQAAAAABLl3i0lAwDQYwSQY2ZNWvOf2k</x509certificate> <x509data>
</keyinfo>
</identity>

The trick is to find a way to send my token to their WCF web service. Add a Service Reference to their WCF web service in the usual way. My example uses MyWS.Service with the url of http://company.co.uk/service.svc.  Note that when I setup the binding to this WCF service I have to include a reference to the issuer of the token (the url to their STS). I also needed to include a reference to the DNS identity.  You can usually assume the DNS Identity from the Subject name on the third party certificate so if that is CN=company.co.uk then the DNS Identity will be company.co.uk.

// add constants for their STS service and DNS identity
public const string ServiceAddress = "http://company.co.uk/service.svc";

public const string DNSIdentity = "company.co.uk";
public const string STSAddress = "http://security.company.co.uk/security.svc";


 public void RetrieveData(SecurityToken smToken)
{
   // Instantiate the ChannelFactory as usual.
   //Be sure to set the DNS Identity on the Endpoint

   EndpointAddress endpointAddress = new EndpointAddress(new Uri(ServiceAddress), new DnsEndpointIdentity(DNSIdentity), new AddressHeaderCollection());

   ChannelFactory clientFactory = new ChannelFactory(GetServiceBinding(ServiceAddress), endpointAddress);
   clientFactory.Credentials.SupportInteractive = false;

   // Make sure to call this prior to using the
   //CreateChannelWith...()
   // extension methods on the channel factory that the Windows
   //Identity Foundation provides.
   clientFactory.ConfigureChannelFactory();
   ICommunicationObject channel = null;

   bool succeeded = false;

   try
   { // create an instance of the Pension Service client

      MyWS.Service client = clientFactory.CreateChannelWithIssuedToken(smToken);
      channel = (ICommunicationObject)client;
      // Now its plain sailing
      // I can call the method on the WCF service
      // in may case GetData returns an array of objects
      MyWS.MyObject[] psArray = client.GetData();
      // TO DO something with psArray
      channel.Close();
      succeeded = true;
   }

   catch (CommunicationException e)
   {  // TO DO log error
      channel.Abort();
   }
   catch (TimeoutException)
   {  // TO DO log error
      channel.Abort();
   }
   finally
   {
       if (!succeeded && channel != null)
       {
          channel.Abort();
       }
    }
   return ;
}


public Binding GetServiceBinding(string uri)
{
   // Use the standard WS2007FederationHttpBinding
   WS2007FederationHttpBinding binding = new WS2007FederationHttpBinding();
   binding.Security.Message.IssuerAddress = new EndpointAddress(STSAddress);
   binding.Security.Message.IssuerBinding = GetSMSecurityTokenServiceBinding(STSAddress);
   binding.Security.Message.IssuerMetadataAddress = new EndpointAddress(STSAddress + "/mex");
   return binding;


Good luck. In terms of difficulty on a scale of 1-10 this is a twelve. I wish you success.

No comments: