In this blog post I am going to walkthrough the steps required to secure a WCF service with ADFS 2.0, as well as how to call this service from a client. The aim of this article is to provide an outline of the general principals and steps, not to explain how to set up a fully secured scenario (everything in this post was tested across a couple of VMs on a development machine).

With federated security, there are two modes of authentication:

  • Passive. In this scenario, a client application (i.e. browser) tries to access a web resource (e.g. a webpage). If the client isn’t authenticated for that resource, it is redirected to a login page. On successful authentication, the client is redirected back to the original web resource and authorized as appropriate. Note that in this scenario, the client browser just needs to follow redirects etc. and does not need to use any special logic to authenticate. A more comprehensive explanation can be found here: http://msdn.microsoft.com/en-us/magazine/ff872350.aspx 
  • Active. In this scenario, it is probable that the resource being accessed does not have a user interface (e.g a WCF / Rest service). The client application therefore needs to contain logic to authenticate against a Secure Token Store. Note that WCF does make provision for the active scenario through config (tutorial here: http://msdn.microsoft.com/en-us/gg557876). In our walkthrough below, however, we are going to authenticate programmatically.

This walkthrough was carried out on two VMs.

  • Server 1: Server 2008R2. Active Directory Domain Controller with IIS7 installed. It is assumed that an AD domain has been set up. This machine will be used to host ADFS2.0.
  • Server 2: Server 2008R2. IIS7 installed. This machine will be used to host our WCF service (note that this does not need to be joined to our domain).

Server 1: Prepare the default website to host the ADFS 2.0 endpoints.

  • When installed (later in this post), ADFS 2.0 creates endpoints under the default website. On the first VM (with AD installed), launch IIS Manager and ensure that the default website is running.
  • Ensure that you have a url that resolves to this machine (to be used for the ADFS 2.0 endpoints).
    • In my environment, I added the following entry to your hosts file (C:\Windows\System32\drivers\etc): 127.0.0.1 adfs.testdomain.dev
    • On a live environment, this should be done via DNS.
    • Note that if you’re LAN connection uses a proxy server, you may need to bypass the proxy for the above address (i.e. IE –> Tools > Options >
  • As ADFS 2.0 endpoints should be run over https, ensure that you have a certificate setup (and it’s trusted). On a production environment this should be properly created and trusted, however on my dev environment I followed the instructions on http://www.robbagby.com/iis/self-signed-certificates-on-iis-7-the-easy-way-and-the-most-effective-way/ using SelfSSL.exe (note that selfssl is part of the IIS Resource Kit Toolkit). 
    • My specific command was “SelfSSL /N:CN=adfs.testdomain.dev /V:365 /S:1” (this create the certificate and configure the default website to use this certificate).

Server 1: Setting up ADFS 2.0

To set up ADFS 2.0, you first need to download it from http://www.microsoft.com/download/en/details.aspx?displaylang=en&id=10909. Once download, run through the following steps…

  • Run the setup wizard
  • On the first screen click next
  • Read and accept the license agreement
  • On the server role screen, select “Federation Server”
  • Click next to install.
  • Once it has run, you will need to restart the machine.

After it has installed, you now need to configure it

  • Launch AD FS 2.0 Management Tool from Start > Administrative Tools > AD FS 2.0 Management
  • Once loaded, select the AD FS 2.0 Federation Server Configuration Wizard (from the main panel)

ADFSSetup1_2_2BB1F561

  • Select create a new federation service and click next

ADFSSetup2_2_2BB1F561

  • Select “Stand-alone federation server” and click next

ADFSSetup3_2_2BB1F561

  • Accept the defaults for SSL certificates (this will be the certificate of you created earlier). Press next

ADFSSetup4_2_2BB1F561

  • Confirm the settings and press next.
  • Wait for the results.

Server 2: Setting up the website to host the WCF Service

On server 2, the WIF run time and SDK first needs to be installed.

Next, if your not using a DNS server (i.e. you’re on a dev enviroment), edit C:\Windows\System32\drivers\etc\hosts and add the following entries:

  • adfs.testdomain.dev – 10.10.10.1 (set this to the IP address of the ADFS VM, aka Server 1)
  • services.testdomain.dev – 127.0.0.1

Next, create a website to host the WCF service:

  • In IIS Manager, right click on the site node in the connections pane and select “Add New Site”. Set the website settings as follows:
    • SiteName: wcftestsite (lowercase)
    • Physical Path: C:\inetpub\wwwroot\wcftestsite (you will need to create this folder)
    • Binding (we will change this to https later)
      • Type: http
      • IP Address: All Unassigned
      • Port: 80
      • Host name: services.testdomain.dev (lowercase)
  • Once created, in IIS Manager, select the “Application Pool” node in the connections pane. Locate and select the newly created wcftestsite application pool.
    • In the basic settings ensure that the application pool .NET framework version is set to v4.0
    • In the “advanced settings” dialog. Under the process model section, change the identity to LocalSystem (this is so that it can access the ADFS certificate in the trusted root store). Note, on a production environment you would use a specific named account as opposed to this.
    • In the “advanced settings” dialog. Under the process model section, set “Load User Profile” to true.

Next, we need to set up a trusted certificate for our new site. We will follow the steps as per server 1 to do this.  My SelfSSL command is as follows: SelfSSL /N:CN=services.testdomain.dev /V:365 /S:2

Also, set IE to accept certificates as above to both services.testdomain.dev and adfs.testdomain.dev (as per server 1).

Server 2: Creating the WCF service

  • Open Visual Studio 2010
  • Select file > new > website
  • Select WCF Service from the project types
  • Set the web location to http://services.testdomain.dev/wcfservice and click ok (all lowercase)
  • Alter / add the following code to the GetData method on your WCF service. Browse to the service to ensure it no errors are thrown.
   1: public string GetData()
   2: {
   3:     var id = Thread.CurrentPrincipal.Identity as IClaimsIdentity;
   4:     return string.Format("Hello {0}", id.Claims[0].Value);
   5: }
  • In IIS Manager, select the newly created website and change the app pool to “wcftestsite”

Now the service has been created, it now needs to be altered to use ADFS 2.0.

  • In Visual Studio, right click on the project node for your WCF service website and select “Add STS Reference” (this is a new option provided by the WIF SDK)

WCF_STS1_2_2BB1F561

  • Click next

WCF_STS2_2_2BB1F561

  • Select the “Use an Existing STS” radio button
  • Set the url to https://adfs.testdomain.dev
  • Click test location (this should autocomplete the rest of the URL as per the screenshot below).
  • Click next

WCF_STS3_2_2BB1F561

  • Set “Disable certificate chain validation” as we have created our own certificates.
  • Click next.

WCF_STS4_2_2BB1F561

  • Select “Enable encryption” (wcf does not allow this to be disabled)
  • Select and existing certificate from store and select you “services.testdomain.dev” cert.
  • Click next
  • Leave offered claim as-is and click next
  • Verify the summary and click finish.
  • Once complete, a folder structure and a file named FederationMetadata.xml will be added to you website.

Server 1: Setup a trust relationship for our WCF service

  • Copy the newly created FederationMetadata.xml file create above to the ADFS VM
  • In ADFS2.0 Management console, from the tree right click on “Trust Relationships” and select “Add Relying Party Trust”.

RelyingPartyTrust1_2_2BB1F561

  • Click start

RelyingPartyTrust2_2_2BB1F561

  • Select “Import data about the relying party from a file” and select the FederationMetadata.xml file copied from the other server. Press next
  • Type the display name as “servicestestdomain” then click next
  • On the next screen, leave permit all users to access this relying party selected. Click next
  • Verify settings and click next
  • Leave the checkbox select for “Open the Edit Claim Rules dialog for this relying party trust when the wizard closes”. Click close.

Edit Claim Rules

  • On the edit claims rules dialog, verify that “Permit Access to All users” is set on the Issuance Authorization Rules tab
  • On the Issuance Tranform Rules tab, click add rule. Select “Pass Through or Filter an Incoming Claim”.
  • Set the claim rule name to “WCFServicePassthroughIncomingClaim”, on incoming claim type, select “Windows Account Name”.
  • Leave the radio button set as “pass through all claim values”
  • Click finish, then ok to save.

Server 2: Setup a test application to call the service

  • Run Visual Studio 2010 as an administrator (this should ensure that we have rights to see our newly created service). Open the solution that contains our WCF Service.
  • In our Visual Studio solution, create a console app named “WCFClient”. Set this as the startup project. Note that this can be totally separate to our solution, even on a different server – it just makes it easier to have them in the same place for this walkthrough.
  • Ensure that the target framework for this project is “.NET Framework 4.0” and not the “.NET Framework 4.0 Client Profile”.

TestRunner2_2_2BB1F561

TestRunner1_2_2BB1F561

  • Set the namespace to MyTestService and click ok.
  • Add a couple of references:
    • System.IdentityModel (.NET 4.0)
    • Microsoft.IdentityModel
  • Add a new class called Token.cs to the project. This actively obtains the token from ADFS 2.0 (the secure token store). Add the following code to this class…
   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.IdentityModel.Tokens;
   6: using System.Net;
   7: using Microsoft.IdentityModel.Protocols.WSTrust;
   8: using System.ServiceModel;
   9: using System.ServiceModel.Security;
  10:  
  11: namespace WCFClient
  12: {
  13:     internal class Token
  14:     {
  15:         public static SecurityToken GetToken(string username, string password, string appliesTo, out RequestSecurityTokenResponse rsts)
  16:         {
  17:             WS2007HttpBinding binding = new WS2007HttpBinding();
  18:             binding.Security.Message.EstablishSecurityContext = false;
  19:             binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.None;
  20:             binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
  21:             binding.Security.Mode = SecurityMode.TransportWithMessageCredential; 
  22:  
  23:             WSTrustChannelFactory trustChannelFactory = new WSTrustChannelFactory(binding, new EndpointAddress("https://adfs.testdomain.dev/adfs/services/trust/13/usernamemixed"));
  24:             trustChannelFactory.TrustVersion = TrustVersion.WSTrust13;
  25:             trustChannelFactory.Credentials.UserName.UserName = username;
  26:             trustChannelFactory.Credentials.UserName.Password = password;
  27:             trustChannelFactory.ConfigureChannelFactory();
  28:  
  29:             // Create issuance issuance and get security token 
  30:             RequestSecurityToken requestToken = new RequestSecurityToken(WSTrust13Constants.RequestTypes.Issue);
  31:             requestToken.AppliesTo = new EndpointAddress(appliesTo);
  32:             WSTrustChannel tokenClient = (WSTrustChannel)trustChannelFactory.CreateChannel();
  33:             SecurityToken token = tokenClient.Issue(requestToken, out rsts);
  34:             return token;
  35:         }
  36:     }
  37: }
  • Add another class called FederatedWCFClient.cs to the project. This effectively wraps our service reference proxy class (autogenerated when we add our service reference), and allows it pass in a token to authenticate. Code for this is as follows:
   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.ServiceModel;
   6: using System.IdentityModel.Tokens;
   7: using Microsoft.IdentityModel.Protocols.WSTrust;
   8:  
   9: namespace WCFClient
  10: {
  11:     public class FederatedWCFClient<T>
  12:     {
  13:         private SecurityToken token;
  14:         private ChannelFactory<T> factory;
  15:         
  16:         public FederatedWCFClient(SecurityToken securityToken, string binding) 
  17:         {
  18:             this.token = securityToken;
  19:             this.factory = new ChannelFactory<T>(binding);
  20:             this.factory.ConfigureChannelFactory<T>();
  21:         }
  22:  
  23:         public void Close()
  24:         {
  25:             this.factory.Close();
  26:         }
  27:  
  28:         public T Client
  29:         {
  30:             get
  31:             {
  32:                 return this.factory.CreateChannelWithIssuedToken(token);
  33:             }
  34:         }
  35:  
  36:         public T ClientActAs
  37:         {
  38:             get
  39:             {
  40:                 return this.factory.CreateChannelActingAs(this.token);
  41:             }
  42:         }
  43:     }
  44: }

Finally, update our main method in Program.cs as follows (ensure that the binding name, endpoint and credentials are set up correctly).

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Net;
   6: using Microsoft.IdentityModel.Protocols.WSTrust;
   7:  
   8: namespace WCFClient
   9: {
  10:     class Program
  11:     {
  12:         static void Main(string[] args)
  13:         {
  14:             var requestTokenResponse = new RequestSecurityTokenResponse();
  15:             var token = Token.GetToken(@"mydomain\testuser", "p@ssw0rd", "http://services.testdomain.dev/wcfservice/Service.svc", out requestTokenResponse);
  16:             
  17:             var wcfClient = new FederatedWCFClient<MyTestService.IService>(token, "WS2007FederationHttpBinding_IService");   // This must match the app.config
  18:             var client = wcfClient.Client as MyTestService.IService;
  19:             var result = client.GetData();
  20:             Console.WriteLine(result);
  21:             wcfClient.Close();
  22:         }
  23:     }
  24: }

Now when you run the program, the client app will do the following

  1. Obtain a token from ADFS
  2. Call the WCF service method
  3. Write the output of the WCF service method

Troubleshooting

  • The most common cause of error when creating this application was due to incorrect / inconsistent casing of the wcf service endpoint url. Verify case consistency of these.
  • ADFS 2.0 logs are visible in the Event Viewer (under “application and service logs”)
  • It can be useful turn on WCF logging WCF http://msdn.microsoft.com/en-us/library/aa702726.aspx. Logs can be opened in the Microsoft Service Trace Viewer (note that I normally use the message view within this – I locate errors,  then view the exception tree for that error).
  • If when browsing to the WCF service endpoint, you receive “CryptographicException: Keyset does not exist” – this usually means the application pool does not trust the certificate. Check the WCF service is running under the correct application pool.

Thanks to Bradley Cotier for his debugging assistance

Written by Rob Nowik