Identity Delegation with AD FS 2.0 Step-by-Step Guide

Applies To: Active Directory Federation Services (AD FS) 2.0

The goal of this step-by-step guide is to walk you through the complete end-to-end experience of developing a composite application that uses the identity delegation feature of Windows® Identity Foundation (WIF) and Active Directory® Federation Services (AD FS) 2.0.

The application consists of two service tiers and a browser-based client. The middle tier is implemented as a Microsoft ASP.NET Web application, and the back end is implemented as a Windows Communication Foundation (WCF) service. Both tiers are implemented as claims-aware applications, using both WIF and AD FS 2.0 for their authentication needs.

The goal is to allow the middle tier to call the back-end service, acting as the caller that is accessing the middle tier, and to accomplish that more securely without modifying the application contracts between the middle tier and back end or between the middle tier and the end user. The following diagram shows the overall application layout with the authentication data flow.

Also note that the claim requirements for the middle tier are different than for the back-end service. The Web front-end application requires Name and User Principal Name (UPN) claims, and the back-end service requires Name, UPN, and Group claims. This guide demonstrates how to set up the claims transformation rules at the AD FS 2.0 Federation Service to implement such scenarios.

For the purposes of this guide, you will use only a single-domain environment, and you will need four computers for the scenario. The following diagram shows the network topology.

Setup and configure the VM test lab

Before you can begin walking through the steps outlined in this guide, you will need to first create and configure four base virtual machines (VM)s using the configurations and suggested computer names as provided in the following table.

Names for the lab VMs Required role or software to be installed

contosodc.contoso.com

This server computer will be a domain controller with the following server roles added and configured for the Contoso.com domain:

  • Active Directory Domain Services (AD DS)

  • Domain Name System (DNS)

  • Active Directory Certificate Services (AD CS)

You will also need to install the AD FS 2.0 software on this computer. Do not add the Active Directory Federation Services (AD FS) role using Server Manager.

contososrv1.contoso.com
contososrv2.contoso.com

These two member server computers will both need to be configured similarly with the following server roles added and configured for the Contoso.com domain:

  • Web Server (IIS)

  • Application Server

In addition, both computers will require Microsoft® Visual Studio® 2010 Professional (or Team Foundation) and the Windows Identity Foundation (WIF) 4.0 SDK to be installed.

contosoc1.contoso.com

This will be a separate client or server computer that is used to access the Web front-end application through Internet Explorer®.

Important

The instructions in this guide do not walk you through the steps for how to create the four VMs indicated in the previous table. However, you can refer to the How to Set Up the AD FS 2.0 VM Lab Environment for Federated Collaboration (https://go.microsoft.com/fwlink/?LinkId=205539) guide to help understand the order and process for how to install and configure the software required for a similar VM lab environment.

Considerations when using the federated collaboration how to guide as a reference

If you decide to use the How to Set Up the AD FS 2.0 VM Lab Environment for Federated Collaboration as a reference for building your VM lab environment you should consider the following before you get started:

  • Important: The federated collaboration how to guide does not use the sts1.contoso.com name for the name of the AD FS 2.0 Federation Service. Instead, it uses contosodc.contoso.com as the name. Because the service principal name (SPN) host/contosodc.contoso.com is already registered on the computer account for the CONTOSODC computer, the AD FS 2.0 configuration wizard indicates that it cannot register the SPN for the AD FS 2.0 service account (CONTOSO/adfssrvc). To work around this issue, register the SPN for CONTOSO\adfssrvc manually by using the following command:

    setspn -S http/contosodc.contoso.com adfssrvc

    After the AD FS 2.0 configuration is finished, run this command with administrative credentials at a command prompt. If it is successful, the output of the setspn utility should look like the following:

    After the installation, create the user accounts based on the previous image and make CONTOSO\Fred the administrator on the CONTOSOSRV1 and CONTOSOSRV2 computers. Use the same password that you used for the other accounts in the guide (p@ssw0rd).

Note

Ensure that you skip the Microsoft SharePoint® installation steps in the federated collaboration how to guide since you will not use SharePoint Products and Technologies in this guide. Installing it modifies the default Web site inside IIS, making it unusable for the purposes of this guide.

  • When you install AD FS 2.0 on the domain controller (use contosodc.contoso.com as the name for the Federation Service instead of sts1.contoso.com, as the federated collaboration how to guide suggests doing. In addition, ensure that you provision the Secure Sockets Layer (SSL) certificates on CONTOSODC (before you install AD FS 2.0) and that both CONTOSOSRV1 and CONTOSOSRV2 have SSL certificates in place. You can make them by using the domain certificate services included in the Internet Information Services (IIS) Management console. The federated collaboration how to guide provides instructions for how to create certificates as needed.

  • The IIS SSL certificates for CONTOSOSRV1 and CONTOSOSRV2 must have the common name (CN) part set to their host names (contososrv1.contoso.com and contososrv2.contoso.com, respectively). For the CONTOSODC, request a certificate with a CN set to contosodc.contoso.com and not *.contoso.com, as the federated collaboration how to guide suggests doing.

Prepare and configure the Hyper-V server for the new VMs

Once you have created your four base VMs as discussed previously, you can use the following procedures to create a virtual network in Hyper-V™ and import the VMs.

Note

You can import the VMs only on a Windows Server® 2008 R2 computer.

To create the virtual network for the AD FS 2.0 VM lab environment

  1. On the host computer, open Hyper-V Manager.

    To open Hyper-V Manager, on the Start menu, point to Administrative Tools, and then click Hyper-V Manager.

  2. In Hyper-V Manager, on the Action menu, click Virtual Network Manager.

  3. In Virtual Network Manager, click Internal for the type of virtual network that you want to create, and then click Add.

  4. In New Virtual Network, for Name, type Internal-Network, verify that for Type, the Internal only option is selected, and then click OK.

Note

The network name is case sensitive and should be entered exactly as noted above. All four VMs must use this network, which is a "local only" interface. All four VM images should already be IP-configured as described in the following section.

To import the AD FS 2.0 lab VMs

  1. In Hyper-V Manager, on the Action menu, click Import Virtual Machine.

  2. In the Import Virtual Machine dialog box, click Browse.

  3. In the Select Folder dialog box, browse to and locate the named folder for the VM that you want to import.

    For example, to import the CONTOSODC VM, navigate to C:\VM, select the ContosoDC folder, and then click Select Folder.

  4. For the Settings, keep the Move or restore the virtual machine setting selected.

  5. Click Import to begin importing the VM.

  6. Repeat these steps for each VM until all VMs have been imported.

Setting Up IIS Bindings at CONTOSOSRV1 and CONTOSOSRV2

Follow these steps on both the CONTOSOSRV1 and CONTOSOSRV2 computers.

Click Start, point to Administrative Tools, and then click IIS Manager.

Select the Default Web Site node.

Click Bindings.

Select the HTTPS binding, if present, and then click Edit. If the HTTPS binding is not present, click Add.

In the SSL certificate list, select the certificate for contososrv1.contoso.com (for the CONTOSOSRV2 computer, select contososrv2.contoso.com). Click OK.

Configuring Application Pool Settings at CONTOSOSRV1

This step is required for IIS 7.5 only (in Windows 7 or Windows Server 2008 R2) on the CONTOSOSRV1 computer.

In IIS Manager, select the Application Pools node, and then select DefaultAppPool.

Click Advanced Settings.

Set the Load User Profile property to True, and then click OK.

Repeat the same property setting change for the ASP.NET 4.0 application pool.

In the list of Application Pools, select ASP.NET v4.0.

Click Advanced Settings.

Set the Load User Profile property to True, and then click OK.

Setting Up Local Intranet Sites on CONTOSOSRV1 and CONTOSOC1

Do this step on CONTOSOSRV1 and CONTOSOC1 (and any other computers that you are using for testing the Web front-end [WFE] application).

Start Internet Explorer, select Tools, and then select Internet Options.

On the Security tab, click Local intranet, and then click Sites.

Click Advanced.

Under Websites, add https://contosodc.contoso.com and https://contososrv1.contoso.com to the list of sites. You can also add https://contosodc.contoso.com, but it is not required for this guide.

Click Close. Click OK, and then click OK again.

Creating a WFE Claims-Aware Web Application on CONTOSOSRV1

On the CONTOSOSRV1 computer, log on as CONTOSO\Fred.

Ensure that you start Visual Studio 2010 as Administrator (to do this, right-click the Visual Studio 2010 icon, and then click Run as Administrator).

In Visual Studio 2010, on the File menu, point to New, and then click Web Site.

In the Language list, select Visual C#. In the Visual Studio installed templates list, select Claims-aware ASP.NET Web Site. Set the Location to HTTP, and then click Browse.

Click Local IIS, and expand Default Web Site. Click the Create New Web Application button, and then type WFE as a name for the new Web application. Select the Use Secure Sockets Layer check box at the bottom, and then click Open.

Change the Location from https://localhost/WFE to https://contososrv1.contoso.com/WFE, and then click OK.

Solution Explorer should appear as follows:

Enure that you save the project and the solution.

You can then press F5 and test the application. You can leave the password field blank in the logon page.

In the next step, you register the new Web application with the AD FS 2.0 service running on CONTOSODC.

Registering the WFE Application with AD FS 2.0 at CONTOSODC

On the CONTOSOSRV1 computer, log on as Fred, and start Visual Studio 2010 with administrative credentials (right-click the Visual Studio 2010 icon, and select Run as Administrator).

In Solution Explorer, right-click the WFE project, and then select Add STS reference.

Note

If you do not see the Add STS Reference option displayed, it might mean that you need to update the Visual Studio Add-In Manager for WIF (Microsoft.IdentityModel.Tools.VS.VSAddin.Addin) so that it is able to locate the Federation Utility (Microsoft.IdentityModel.Tools.FedUtil.dll). You can manually fix this in Visual Studio 2010, by performing the following steps: From the Tools menu, click Options. Under Environment, select Add-in/Macros Security. In the listed add-in file paths, verify that the following path is listed:
ProgramFiles(x86)%\Windows Identity Foundation SDK\v4.0\Visual Studio Extensions
If it is not listed, click Add to add it to the list. You might also have to close nad restart Visual Studio 2010 in order for this change to take effect.

Verify that the values for Application configuration location and Application URI (uniform resource identifier) are set correctly, and then click Next.

Select Use an existing STS, and set the STS federation metadata location URL to https://contosodc.contoso.com/FederationMetadata/2007-06/FederationMetadata.xml. To verify the URL, click Test location. Internet Explorer opens the WS-Federation metadata document published by AD FS 2.0. If the document does not appear, verify that you typed the URL correctly as listed previously. After you verify that the URL works, click Next.

The AD FS 2.0 service in this guide is configured with an automatically managed self-signed certificate that cannot be chained to any certification authority that the computer trusts. To allow the WFE service to consume tokens that are issued by this Federation Service, you must disable chain-trust verification. Ensure that Disable certificate chain validation is selected, and then click Next.

Select No encryption, and then click Next.

Click Next again.

You can preview the actions that the Federation Utility will perform in this window. In this guide, you will not schedule an automated task to update the application configuration based on the federation metadata document. Click Finish.

After the Federation Utility finishes, the application configuration is updated, and a federation document that describes the application authentication requirements is created. You can verify this in Solution Explorer.

The two main changes that are made in the Web.config file are adding the WSFederationAuthenticationModule and SessionAuthenticationModule into the ASP.NET pipeline, and configuring the WIF runtime using the microsoft.identityModel section.

Ensure that you save both the project and solution for the WFE application. (You can leave Visual Studio 2010 open because you will be returning to use it again in the next section.)

Completing the Relying Party Registration at CONTOSODC

Next, you must register the application with the AD FS 2.0 installation that is running at CONTOSODC. Switch to the CONTOSODC computer, log on as CONTOSO\Administrator, and start the AD FS 2.0 console.

Select the Relying Party Trusts node.

In the Actions list, click Add Relying Party Trust.

When the Add Relying Party Trust Wizard appears, click Start.

Select Import data about the relying party published online or on a local network. In the Federation metadata address box, type https://contososrv1.contoso.com/WFE/FederationMetadata/2007-06/FederationMetadata.xml. This is the location of the federation metadata document that was created by the Federation Utility in the previous section. Click Next.

Change the display name to signal that this entry represents the WFE application at contososrv1.contoso.com. You can also add some notes that are associated with this relying party trust. Click Next.

Verify that Permit all users to access this relying party is selected, and then click Next.

On the next page, you can preview the information about the relying party. Then, click Next.

Ensure that the Open the Edit Claim Rules dialog for this relying party trust when the wizard closes check box is selected, and then click Close.

The Edit Claim Rules dialog box appears. On the Issuance Transform Rules tab, click Add Rule.

In the Claim rule template list, select Send LDAP Attributes as Claims, and then click Next.

In the Claim rule name box, type Issue UPN and Name. In the Attribute store list, select Active Directory. Configure the rule to issue the User-Principal-Name LDAP Attribute as the UPN claim type and the Display-Name LDAP Attribute as the Name claim type. Click Finish.

The Issuance Transform Rules should now contain one rule that you just created. Click OK.

After you close the Edit Claim Rules dialog box, a new relying party trust appears in the AD FS 2.0 Management console under the Relying Party Trusts node.

You have now completed the registration procedure for the WFE application at the AD FS 2.0 installation. To verify that everything is set up correctly, go to the CONTOSOSRV1 computer and, in Visual Studio, press F5. Internet Explorer should start.

After the authentication finishes, you should see UPN and Name claims that belong to the CONTOSO\Fred account along with authentication instant and authentication method claims. You can also switch to the CONTOSOC1 computer and test the application by using the CONTOSO\Bob account by going to https://contososrv1.contoso.com/WFE.

Enabling the Application Pool Account to Access the SSL Certificate at CONTOSOSRV2

Because the Federation Utility requires the WCF service to use an X.509 certificate to decrypt incoming security tokens issued by the Federation Service, you will use the SSL certificate that is already provisioned on the CONTOSOSRV2 computer for this purpose. To do this, you must enable the processes that are running under the IIS application pool account to access the private key of this certificate.

First, you will need to create a new version of IIS7Util.exe, a free utility provided in the WIF SDK 4.0 utility scripts. The following steps will walk you through the process.

  1. In Windows Explorer, open to the %ProgramFiles(x86)%\Windows Identity Foundation SDK\v4.0\Samples\Utilities\Scripts\ folder.

  2. Start Notepad, copy and paste the following batch file code into Notepad, and save the file using the name of configure_cert_key.bat.

    Save the file in the folder that you opened in the previous step.

    @echo off
    setlocal
    set WIFSDKSCRIPTS=C:\Program Files (x86)\Windows Identity Foundation SDK\v4.0\Samples\Utilities\Scripts\
    set CERTHASH=
    set KEYCONTAINER=
    set IIS7UTIL=IIS7Util.exe
    set CERTSUBJECTNAME="contososrv2.contoso.com, OU=IT, O=Contoso, L=Redmond, S=WA, C=US"
    
    pushd "%WIFSDKSCRIPTS%"
    
    for /f %%a in ('%IIS7UTIL% GetCertThumbprint my %CERTSUBJECTNAME%') do (set CERTHASH=%%a)
    
    for /f %%a in ('%IIS7UTIL% GetCertKeyContainer my %CERTHASH%') do (set KEYCONTAINER=%%a)
    
    popd
    
    pushd "%ALLUSERSPROFILE%\Microsoft\Crypto\RSA\MachineKeys"
    
    icacls %KEYCONTAINER% /grant *S-1-5-20:R
    icacls %KEYCONTAINER% /grant "IIS APPPOOL\DefaultAppPool":R
    icacls %KEYCONTAINER% /grant "IIS APPPOOL\ASP.NET v4.0":R
    
    popd 
    
  3. Rename the IIS7Util.* files (IIS7Util.cs, IIS7Util.exe) in the same folder.

  4. In Notepad, create a new file using the contents of the Appendix: Full code sample for modified IIS7Util.cs at the end of this guide, and save it as IIS7Util.cs.

  5. Next, open a new Command Prompt window with administrative credentials (on the Start menu, right-click the Command Prompt shortcut, and then select Run as Administrator).

  6. Change to the directory where you saved the previous files and set the environment variables for Microsoft Visual C#® command-line operation. To do this, run Vsvars32.bat, which may require you to specify the full path to it that will typically be the %ProgramFiles(x86)%\Microsoft Visual Studio 10.0\Common7\Tools folder.

  7. Use the following command line to compile the modified IIS7Util.exe:

    csc IIS7Util.cs /reference:C:\Windows\system32\inetsrv\Microsoft.Web.Administration.dll
    
  8. Run the configure_cert_key.bat file.

    If everything works correctly, the script will print two messages about setting ACLs on the private key file that corresponds to the SSL X.509 certificate. These messages come from the Icacls utility that the script file uses.

Creating the WCF Back-End Service at CONTOSOSRV2

On the CONTOSOSRV2 computer log off as CONTOSO\Administrator, and log back on as CONTOSO\Fred. Ensure that the HTTPS binding is added to the default Web site in IIS Manager on CONTOSOSRV2 and that the contososrv2.contoso.com certificate is selected as the server's SSL certificate for this HTTPS binding.

Start Visual Studio 2010 with administrative credentials (right-click the Visual Studio 2010 icon, and select Run as Administrator).

First, you must create a new claims-aware WCF service. On the File menu, point to New, and then select Web Site.

In the New Web Site dialog box, in the Language list, select Visual C#. In the Templates list, select Claims-aware WCF Service. In the Location list, select HTTP, and then click Browse.

Select Local IIS, and under the Local Web Servers node, click the Default Web Site. Click the New Web application button. Type BackendService as the name of the new Web application. Select the Use Secure Sockets Layer check box, and then click Open.

Change the location value to https://contososrv2.contoso.com/BackendService, and then click OK.

In Solution Explorer, open the IService.cs and Service.cs files from the App_Code node. You must change the default implementation of the service to return the information that you need.

Change the IService.cs file as follows:

using System.ServiceModel;
 
namespace BackendService
{
    [ServiceContract]
    public interface IService
    {
        [OperationContract]
        string GetData(string value);
    }
}

You might also need to comments out the following lines if they appear in IService.cs in order to resolve compiler errors if they should occur when you build:

        // [OperationContract]
        // CompositeType GetDataUsingDataContract(CompositeType composite);

Change the Service.cs file as follows:

using System.Text;
using System.Threading;
using Microsoft.IdentityModel.Claims;
 
namespace BackendService
{
    public class Service : IService
    {
        public string GetData(string value)
        {
            IClaimsPrincipal principal = (IClaimsPrincipal)Thread.CurrentPrincipal;
            IClaimsIdentity identity = (IClaimsIdentity)principal.Identity;
            StringBuilder sb = new StringBuilder();
 
            // Compute the result based on the input value.
            sb.AppendFormat("<div><p>You entered: <b>{0}</b></p>\n", value);
 
            // Print details about the caller's identity.
            sb.AppendFormat("<p>Caller's identity name: {0}</p>\n", 
                identity.Name != null ? identity.Name : "Identity does not have a name");
            sb.Append("Caller's claims:<br>\n");
            PrintClaimsTable(sb, identity);
 
            // Print all the actors associated with the caller's identity.
            identity = identity.Actor;
            while (identity != null)
            {
                sb.AppendFormat("<p>Calling via identity: {0}</p>\n", 
                    identity.Name != null ? identity.Name : "Identity does not have a name");
                sb.Append("With claims:<br>\n");
                PrintClaimsTable(sb, identity);
                identity = identity.Actor;
            }
 
            sb.Append("</div>");
 
            return sb.ToString();
        }
 
        private static void PrintClaimsTable(StringBuilder sb, IClaimsIdentity identity)
        {
            sb.Append("<table style='width:100%;'><tr><th>Claim Type</th>" + 
                "<th>Claim Value</th><th>Claim Issuer</th></tr>\n");
            foreach (Claim claim in identity.Claims)
            {
                sb.AppendFormat("<tr><td>{0}</td><td>{1}</td><td>{2}</td></tr>\n",
                    claim.ClaimType, claim.Value, claim.Issuer);
            }
            sb.Append("</table>\n");
        }
    }
}

Ensure that everything is typed correctly by building the solution (click Build, and then click Build Solution).

Registering the BackendService with AD FS 2.0

Next, we will need to register the BackendService application in the project on CONTOSOSRV2 with the AD FS 2.0 installation on CONTOSODC.

Note

If you do not see the Add STS Reference option displayed, it might mean that you need to update the Visual Studio Add-In Manager for WIF (Microsoft.IdentityModel.Tools.VS.VSAddin.Addin) so that it is able to locate the Federation Utility (Microsoft.IdentityModel.Tools.FedUtil.dll). You can manually fix this in Visual Studio 2010, by performing the following steps: From the Tools menu, click Options. Under Environment, select Add-in/Macros Security. In the listed add-in file paths, verify that the following path is listed:
ProgramFiles(x86)%\Windows Identity Foundation SDK\v4.0\Visual Studio Extensions
If it is not listed, click Add to add it to the list. You might also have to close and restart Visual Studio 2010 in order for this change to take effect.

In Solution Explorer, right-click the BackendService project, and select Add STS reference.

Verify that the value for Application configuration location is correct, and change the Application URI to https://contososrv2.contoso.com/BackendService/Service.svc (the value that is offered by the Federation Utility uses an http: URL scheme).

In the Service name list, ensure that BackendService.Service is selected, and in the Endpoint contract name list, ensure that BackendService.IService is selected. Click Next.

Select Use an existing STS, and in the STS federation metadata location box, type https://contosodc.contoso.com/FederationMetadata/2007-06/FederationMetadata.xml. You can click Test location to verify that you entered the correct URL. After you ensure that the URL is correct, click Next.

The AD FS 2.0 installation in this guide is configured with an automatically managed self-signed certificate that cannot be chained to any certification authority that the computer trusts. To allow the back-end service to consume tokens that are issued by this Federation Service, you must disable chain-trust verification. Ensure that Disable certificate chain validation is selected, and then click Next.

The Federation Utility requires that the WCF services use the token encryption feature. Therefore, you must select a certificate that the back-end service will use to decrypt the incoming tokens. You will use the SSL certificate that was already provisioned on the CONTOSOSRV2 computer.

Select Enable encryption. Under Encryption Certificate, click Select an existing certificate from store, and then click Select Certificate.

Select the contososrv2.contoso.com certificate in the list, click OK, and then click Next in the Federation Utility window.

The next window shows the supported claim types that are provided by the AD FS 2.0 instance at CONTOSODC. Click Next.

The next window summarizes the changes that the Federation Utility will perform after you click Finish. Ensure that the Schedule a task to perform daily federation metadata updates check box is cleared, and then click Finish.

The Federation Utility modified the Web.config file. In particular, it updated the BackendService.Service binding to use wsFederation2007HttpBinding, and it configured the WIF runtime by using the microsoft.identityModel section.

Next, you must add the Federation Service endpoint that you want the BackendService clients to use when they request tokens for the BackendService. Because you know that you control all BackendService clients, you can determine an endpoint that works for all of them.

Note

In general, when the clients can come from various locations and use different claims providers, the selection of the endpoint must be done at the client before initiating communication to the service. Applications can use the Windows CardSpace™ component of the Windows operating system to externalize the endpoint selection from the application logic.

To select the Federation Service endpoint for the clients, you must modify the Web.config file in the BackendService project. Locate the message XML element in system.serviceModel/bindings/ws2007FederationHttpBinding/binding/security element. Change the mode attribute value from Message to TransportWithMessageCredential, and add the following element, for example, after the issuerMetadata element (the order of elements in the message element is not important).

              <issuer address="https://contosodc.contoso.com/adfs/services/trust/13/kerberosmixed" />

This causes the BackendService to expose a WSDL document that instructs all its clients to go to the above endpoint when they request tokens for the BackendService. After you make the change, the whole ws2007FederationHttpBinding element should look like the following:

<ws2007FederationHttpBinding>
        <binding name="BackendService.IService_ws2007FederationHttpBinding">
          <security mode="TransportWithMessageCredential">
            <message>
              <issuerMetadata address="https://contosodc.contoso.com/adfs/services/trust/mex" />
              <issuer address="https://contosodc.contoso.com/adfs/services/trust/13/kerberosmixed"/>
              </issuer>
              <claimTypeRequirements>
                <!--Following are the claims offered by Federation Service 'https://contosodc.contoso.com/adfs/services/trust'. Add or uncomment claims that you require by your application and then update the federation metadata of this application.-->
                <add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" isOptional="true" />
                <add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/role" isOptional="true" />
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/claims/CommonName" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/claims/EmailAddress" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/claims/Group" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/claims/UPN" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod" isOptional="true" />-->
                <!--<add claimType="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/denyonlysid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarysid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/denyonlyprimarygroupsid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/groupsid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/primarygroupsid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/primarysid" isOptional="true" />-->
                <!--<add claimType="https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname" isOptional="true" />-->
              </claimTypeRequirements>
            </message>
          </security>
        </binding>
      </ws2007FederationHttpBinding>

Be sure to resave the solution along with these configuration updates after you are done making them.

Completing the Relying Party Registration at the CONTOSODC

Next, you must register the application with the AD FS 2.0 installation that is running at CONTOSODC. Switch to the CONTOSODC computer, log on as CONTOSO\Administrator, and start the AD FS 2.0 console.

Select the Relying Party Trusts node.

In the Actions list, click Add Relying Party Trust.

The Add Relying Party Trust Wizard appears.

Click Start.

Select the Import data about the relying party published online or on a local network option, and in the Federation metadata address box, type https://contososrv2.contoso.com/BackendService/FederationMetadata/2007-06/FederationMetadata.xml. This is the location of the federation metadata document that was created by the Federation Utility in the previous section. Click Next.

Change the Display name to signal that this entry represents a back-end service application at contososrv2.contoso.com. You can also add some notes that are associated with this relying party trust. Click Next.

Verify that Permit all users to access this relying party is selected, and then click Next.

On the next page, you can preview the information about the relying party. Then, click Next.

Ensure that the Open the Edit Claim Rules dialog for this relying party trust when the wizard closes check box is selected, and then click Close.

The Edit Claim Rules dialog box appears. On the Issuance Transform Rules tab, click Add Rule.

In the Claim rule template list, select Send LDAP Attributes as Claims, and then click Next.

Name the rule Issue Group, UPN and Name claims. In the Attribute store list, select Active Directory, and configure the rule to issue the Token-Groups - Qualified by Long domain name LDAP attribute as Group and the User-Principal-Name LDAP attribute as the UPN claim type, and the Display-Name LDAP attribute as the Name claim type. Click Finish.

Click OK.

There should now be two relying party trusts listed in the AD FS 2.0 console.

Enabling Identity Delegation and Fixing Claims Issuance Rules at CONTOSODC

The claims issuance rule that you created in the previous step would work well if the user accessed the back-end service directly. However, in this case, the back-end service is accessed by the WFE ASP.NET application acting as the user.

The WFE Web application sends a token request to the AD FS 2.0 instance at CONTOSODC, authenticating as the application pool account (CONTOSO\CONTOSOSRV1$ in this case), and sending along the security token that it received from its caller (the user who is accessing the WFE application through the browser). The WFE Web application then makes a request to the AD FS 2.0 instance for a token for the back-end service, acting as the user, who is represented by the security token that is included in the request.

The AD FS 2.0 service instance now has two identities—the identity of the token requester, CONTOSO\CONTOSOSRV1$, and the identity of the original caller, CONTOSO\Bob or CONTOSO\Fred—and it must issue claims for both and combine them correctly to issue the resulting token. To transform the identities, it uses the claim issuance rules that are associated with the BackendService relying party for both identities. Therefore, you must ensure that the issuance rules that you associated with the BackendService work for the application pool account and also the identities that represent the callers of the WFE application.

But first, you must allow the WFE application to request a token acting as something else when communicating with the back-end service. For that, you must go back to the BackendService relying party trust. In the AD FS 2.0 console, go to the Relying Party Trusts node, select the BackendService trust, and then click Edit Claims Rules.

Select the Delegation Authorization Rules tab, and then click Add Rule.

In the Claim rule template list, select Permit or Deny Users Based on an Incoming Claim, and then click Next.

In the Claim rule name box, type Allow WFE on contososrv1 to use ActAs feature. In the Incoming claim type list, select Windows account name. In the Incoming claim value box, type CONTOSO\CONTOSOSRV1$ (all uppercase letters). Ensure that Permit access to users with this incoming claim is selected, and then click Finish.

Next, you must update the Issuance Transform rules for the BackendService.

Select the Issuance Transform Rules tab. Currently, you have only one rule in this section to issue Group, UPN, and Name claims from the Active Directory attribute store. If you click Edit Rule and then click View Rule Language in the dialog box, you will see that this rule uses the WindowsAccountName claim issued by the AD AUTHORITY to select the Group, UPN, and Name claims. This works when the user directly authenticates to the Federation Service. However, in this case, the user is represented by a token that was issued by the AD FS 2.0 server to the WFE application. Claims that are extracted from this token do not have the issuer set to AD AUTHORITY because they are not coming from the Windows Local Security Authority (LSA) process. Instead, the issuer is set to SELF AUTHORITY because the claims are coming from a security token that is issued by the Federation Service itself (in this case, to the WFE application).

To make the scenario work end to end, you must add another Issuance Transform Rule that issues Group, UPN, and Name claims based on a claim that comes from the security token with the SELF AUTHORITY issuer. To do this, you must create a custom claims transform rule. First, click Add Rule.

In the Claim rule template list, select Send Claims Using a Custom Rule, and then click Next.

Copy the following code and paste it into the Custom rule text box.

c:[Type == "https://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", Issuer == "SELF AUTHORITY"]
 => issue(store = "Active Directory", types = ("https://schemas.xmlsoap.org/claims/Group", "https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn", 
"https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"), query = ";tokenGroups(longDomainQualifiedName),userPrincipalName,displayName;{0}", param = c.Value);

Name the rule Issue UPN, Name and Group claims using WindowsAccountName from an incoming token, and then click Finish.

The Claims Transform Rules section now has two rules in it. Click OK.

To make the rule that you created in the previous step work, you must ensure that the WindowsAccountName claim is issued for tokens that are targeted at the WFE application so that when the WFE application sends those tokens to the Federation Service inside the ActAs element, the claims transformation rule that you just created works. Select the WFE application relying party trust in the AD FS 2.0 console, and then click Edit Claim Rules.

To add a new rule that issues the WindowsAccountName claim, click Add Rule.

In the Claim rule template list, select Pass Through or Filter an Incoming Claim, and then click Next.

Name the rule Issued WindowsAccountName. In the Incoming claim type list, select Windows account name. Ensure that Pass through all claim values is selected, and then click Finish.

The Issuance Transform Rules section should now have two rules. Click OK.

You have now completed configuring the identity delegation in AD FS 2.0. In the next step, you finish the WFE application at CONTOSOSRV1 and test the solution end to end.

Finishing the WFE Application at CONTOSOSRV1

Switch to the CONTOSOSRV1 computer, and log on as CONTOSO\Fred. Start Visual Studio 2010 with administrative credentials (right-click the Visual Studio 2010 shortcut, and then select Run as Administrator). Open the WFE project.

First, you must add a reference to the BackendService to the WFE project. Right-click the WFE project, and then select Add Service Reference.

In the Add Service Reference dialog box, in the Address box, type https://contososrv2.contoso.com/BackendService/Service.svc, and then click Go. Select the IService contract under the Service node, and in the Namespace box, type BackendServiceReference. Click OK.

In Solution Explorer, verify that the service reference was added to the project. The BackendServiceReference appears under the App_WebReferences node.

Next, you modify the Web.config file to ensure that the correct Federation Service identity is configured for the Kerberos authentication that the WFE application uses to request tokens for the BackendService. Open the Web.config file, and locate the system.serviceModel/bindings/ws2007FederationHttpBinding/binding/security/message/issuer.

Add the following as a child element of the issuer element:

                <identity>
                  <servicePrincipalName value="http/contosodc.contoso.com"/>
                </identity>

After you make the changes, the ws2007FederationHttpBinding looks like the following example:

      <ws2007FederationHttpBinding>
        <binding name="WS2007FederationHttpBinding_IService" 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">
          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
            maxBytesPerRead="4096" maxNameTableCharCount="16384" />
          <reliableSession ordered="true" inactivityTimeout="00:10:00"
            enabled="false" />
          <security mode="TransportWithMessageCredential">
            <message algorithmSuite="Default" issuedKeyType="SymmetricKey"
              negotiateServiceCredential="true">
              <issuer address="https://contosodc.contoso.com/adfs/services/trust/13/kerberosmixed"
                binding="customBinding" bindingConfiguration="https://contosodc.contoso.com/adfs/services/trust/13/kerberosmixed" >
                <identity>
                  <servicePrincipalName value="http/contosodc.contoso.com"/>
                </identity>
              </issuer>
              <issuerMetadata address="https://contosodc.contoso.com/adfs/services/trust/mex" />
              <tokenRequestParameters>
                <trust:SecondaryParameters xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">
                  <trust:KeyType xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://docs.oasis-open.org/ws-sx/ws-trust/200512/SymmetricKey</trust:KeyType>
                  <trust:KeySize xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">256</trust:KeySize>
                  <trust:Claims Dialect="https://schemas.xmlsoap.org/ws/2005/05/identity"
                    xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">
                    <wsid:ClaimType Uri="https://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
                      Optional="true" xmlns:wsid="https://schemas.xmlsoap.org/ws/2005/05/identity" />
                    <wsid:ClaimType Uri="https://schemas.microsoft.com/ws/2008/06/identity/claims/role"
                      Optional="true" xmlns:wsid="https://schemas.xmlsoap.org/ws/2005/05/identity" />
                  </trust:Claims>
                  <trust:KeyWrapAlgorithm xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p</trust:KeyWrapAlgorithm>
                  <trust:EncryptWith xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptWith>
                  <trust:SignWith xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://www.w3.org/2000/09/xmldsig#hmac-sha1</trust:SignWith>
                  <trust:CanonicalizationAlgorithm xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://www.w3.org/2001/10/xml-exc-c14n#</trust:CanonicalizationAlgorithm>
                  <trust:EncryptionAlgorithm xmlns:trust="https://docs.oasis-open.org/ws-sx/ws-trust/200512">https://www.w3.org/2001/04/xmlenc#aes256-cbc</trust:EncryptionAlgorithm>
                </trust:SecondaryParameters>
              </tokenRequestParameters>
            </message>
          </security>
        </binding>
      </ws2007FederationHttpBinding>

While you are editing the Web.config file, you must make another change. Locate the microsoft.identityModel/service element, and change it by adding the following attribute.

saveBootstrapTokens="true"

The changed element should look like the following:

<microsoft.identityModel>
    <service saveBootstrapTokens="true">
      <audienceUris>

This change instructs WIF to store the security token that the caller used to authenticate to the application inside the session state so that the application does not have to. WIF makes this security token available to the application as long as the session between the user and the Web application exists. You can use this cached security token to request tokens for the BackendService acting as the original caller.

Next, you modify the WFE application business logic to integrate it with the BackendService. First, add the Global.asax file and use it to create and cache an instance of the WCF ChannelFactory object. This instance is then used by the Default.aspx page to make calls to the BackendService.

Note

Creating ChannelFactory instances is a process that can consume many computing resources. Because the ChannelFactory can be used to create multiple channels running under different credentials, we recommend that the middle-tier application avoids creating a ChannelFactory instance every time that a new channel is needed.

In Solution Explorer, right-click the WFE project, and select Add New Item.

In the Templates list, select the Global Application Class, and then click Add.

Modify the Global.asax file to look like the following code example:

<%@ Application Language="C#" %>
 
<%@ Import Namespace="System.ServiceModel" %>
<%@ Import Namespace="BackendServiceReference" %>
<%@ Import Namespace="Microsoft.IdentityModel.Protocols.WSTrust" %>
 
<script >
 
    void Application_Start(object sender, EventArgs e) 
    {
        // Code that runs on application startup.
        ChannelFactory<BackendServiceReference.IServiceChannel> service2CF = 
            new ChannelFactory<BackendServiceReference.IServiceChannel>("WS2007FederationHttpBinding_IService");
        service2CF.ConfigureChannelFactory();
 
        Application["WFE_CachedChannelFactory"] = service2CF;
    }
    
    void Application_End(object sender, EventArgs e) 
    {
        //  Code that runs on application shutdown.
 
    }
        
    void Application_Error(object sender, EventArgs e) 
    { 
        // Code that runs when an unhandled error occurs.
 
    }
 
    void Session_Start(object sender, EventArgs e) 
    {
        // Code that runs when a new session is started.
 
    }
 
    void Session_End(object sender, EventArgs e) 
    {
        // Code that runs when a session ends. 
        // Note: The Session_End event is raised only when the sessionstate mode
        // is set to InProc in the Web.config file. If session mode is set to 
        // StateServer or SQLServer, the event is not raised.
 
    }
       
</script>

As you can see, you have implemented the Application_Start method. In doing so, you create a new instance of the ChannelFactory<BackendServiceReference.IServiceChannel>, use the WIF ConfigureChannelFactory extension method (coming from the Microsoft.IdentityModel.Protocols.WSTrust namespace), and finally, store the prepared channel factory in the application state collection.

Next, you modify the Default.aspx and Default.aspx.cs files to use the cached channel factory and expose the identity delegation feature to the WFE application users.

To do so, open the Default.aspx file and modify its source code to look like the following code example:

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="https://www.w3.org/1999/xhtml">
<head id="Head1" >
    <title>Claims-aware ASP.NET Web Site</title>
    <style type="text/css">
        .style1
        {
            font-size: large;
            font-weight: bold;
        }
        div
        {
                font-family: Verdana;
                font-size: small;
        }
        p
        {
                font-size: small;
        }
        table
        {
                font-size: 9px;
                color: Gray;
        }
    </style>
</head>
<body>
    <p class="style1">
        Windows Identity Foundation - Claims-aware ASP.NET Web Site</p>
    <div>
    <asp:Table ID="CallerClaims" />
    </div>
    <form id="form1"  method="post" defaultbutton="Button1" 
    defaultfocus="TextBox1" >
    <div>
        Input value:
        <asp:TextBox ID="TextBox1" ></asp:TextBox>
        <br />
        <asp:Button ID="Button1"  onclick="Button1_Click" 
            Text="Call Backend Service" />
    </div>
    </form>
    <asp:PlaceHolder ID="BackendServiceOutput"  />
</body>
</html>

Next, open the Default.aspx.cs file and modify its content as follows:

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
 
using Microsoft.IdentityModel.Claims;
using Microsoft.IdentityModel.Protocols.WSTrust;
using System.Threading;
using System.IdentityModel.Tokens;
using System.ServiceModel;
using BackendServiceReference;
using System.Text;
 
public partial class _Default : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        IClaimsPrincipal claimsPrincipal = Page.User as IClaimsPrincipal;
        IClaimsIdentity claimsIdentity = (IClaimsIdentity)claimsPrincipal.Identity;
 
        // The code below shows claims found in the IClaimsIdentity.
        // TODO: Change code below to do your processing using claims.
 
        TableRow headerRow = new TableRow();
 
        TableCell claimTypeCell = new TableCell();
        claimTypeCell.Text = "Claim Type";
        claimTypeCell.BorderStyle = BorderStyle.Solid;
 
        TableCell claimValueCell = new TableCell();
        claimValueCell.Text = "Claim Value";
        claimValueCell.BorderStyle = BorderStyle.Solid;
 
        headerRow.Cells.Add(claimTypeCell);
        headerRow.Cells.Add(claimValueCell);
        CallerClaims.Rows.Add(headerRow);
 
        TableRow newRow;
        TableCell newClaimTypeCell, newClaimValueCell;
        foreach (Claim claim in claimsIdentity.Claims)
        {
            newRow = new TableRow();
            newClaimTypeCell = new TableCell();
            newClaimTypeCell.Text = claim.ClaimType;
 
            newClaimValueCell = new TableCell();
            newClaimValueCell.Text = claim.Value;
 
            newRow.Cells.Add(newClaimTypeCell);
            newRow.Cells.Add(newClaimValueCell);
 
            CallerClaims.Rows.Add(newRow);
        }
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        SecurityToken callerToken = null;
        IClaimsPrincipal claimsPrincipal = Thread.CurrentPrincipal as IClaimsPrincipal;
        string value = TextBox1.Text;
        if (String.IsNullOrEmpty(value))
        {
            value = "Default Input";
        }
 
        // Get the Bootstrap Token.
        // We expect only one identity, which will contain the bootstrap token.            
        if (claimsPrincipal != null && claimsPrincipal.Identities.Count == 1)
        {
            callerToken = claimsPrincipal.Identities[0].BootstrapToken;
        }
 
        if (callerToken == null)
        {
            BackendServiceOutput.Controls.Add(new LiteralControl(
                "<b>saveBootstrapTokens must be set to 'true' on the microsoft.identityModel/service element</b>"));
            return;
        }
 
        // Get the channel factory to the back-end service 
        // from the application state.
        ChannelFactory<BackendServiceReference.IServiceChannel> factory =
            (ChannelFactory<BackendServiceReference.IServiceChannel>)Application["WFE_CachedChannelFactory"];
 
        // Create and set up channel to talk to the back-end service.
        BackendServiceReference.IServiceChannel channel;
 
        // Set up the ActAs to point to the caller's token so that we perform a
        // delegated call to the back-end service
        // on behalf of the original caller.
        //
        // Note: A new channel must be created for each call.
        channel = factory.CreateChannelActingAs(callerToken);
 
        string retval = null;
 
        // Call the back-end service and handle the possible exceptions.
        try
        {
            retval = channel.GetData(value);
            channel.Close();
        }
        catch (CommunicationException exception)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<pre>");
            sb.AppendLine(exception.Message);
            sb.AppendLine(exception.StackTrace);
            Exception ex = exception.InnerException;
            while (ex != null)
            {
                sb.AppendLine("===========================");
                sb.AppendLine(ex.Message);
                sb.AppendLine(ex.StackTrace);
                ex = ex.InnerException;
            }
            sb.AppendLine("</pre>");
            channel.Abort();
            retval = sb.ToString();
        }
        catch (TimeoutException)
        {
            channel.Abort();
            retval = "<p>Timed out...</p>";
        }
        catch (Exception exception)
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendLine("<pre>");
            sb.AppendLine("An unexpected exception occurred.");
            sb.AppendLine(exception.StackTrace);
            sb.AppendLine("</pre>");
            channel.Abort();
            retval = sb.ToString();
        }
 
        BackendServiceOutput.Controls.Add(new LiteralControl(retval));
    }
}

This change added a text box and button to the default page and registered an OnClick handler with the button. The event handler uses the cached channel factory instance to construct a new channel, acting as the original caller. To get a representation of the original caller, the event handler uses the BootstrapToken extension property in the IClaimsIdentity class.

            callerToken = claimsPrincipal.Identities[0].BootstrapToken;

The event handler then uses the callerToken to construct the channel by using the CreateChannelActingAs extension method:

        channel = factory.CreateChannelActingAs(callerToken);

Next, it invokes the service by using the value that is provided in the text box, correctly handling any exceptions that this call might cause.

            retval = channel.GetData(value);
            channel.Close();

Finally, it displays the result by using the BackendServiceOutput placeholder control.

        BackendServiceOutput.Controls.Add(new LiteralControl(retval));

Ensure that you save all the modified files and project.

Testing the Final Application

Next, switch to the CONTOSOC1 computer (you can also perform this step from the CONTOSOSRV1 computer), and log on as CONTOSO\Bob. Start Internet Explorer, and in the address bar, type https://contososrv1.contoso.com/WFE, and then press ENTER. After the authentication, you should see the default page that displays Bob's name, UPN, and WindowsAccountName claims along with authentication method and authentication instant claims. Next, type a value in the text box, and then click Call Backend Service. The result of the call appears below the button, as shown in the following image.

Appendix: Full code sample for modified IIS7Util.cs

The following code is to be used to recompile a version of the IIS7Util.exe that can be used when Enabling the Application Pool Account to Access the SSL Certificate at CONTOSOSRV2.

//-----------------------------------------------------------------------------
//
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) 2010 Microsoft Corporation. All rights reserved.
//
//
//-----------------------------------------------------------------------------

using Microsoft.Web.Administration;
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

class IIS7Util
{
    static string usageString = "Usage: IIS7Util.exe [[SetSslCert | DeleteSslCert] <CertificateHash>] | [GetCertThumbprint <storeName> [<*.cer> | 
<subjectName>]] | [GetCertKeyContainer <storeName> <hash>] ";

    static void Main( string[] args )
    {
        if ( args.Length == 2 && args[0].Equals( "SetSslCert", StringComparison.InvariantCultureIgnoreCase ) )
        {
            SetSslCert( args[1] );
        }
        else if ( args.Length == 3 && args[0].Equals( "GetCertThumbprint", StringComparison.InvariantCultureIgnoreCase ) )
        {
            CertThumbprint( args[1], args[2] );
        }
        else if ( args.Length == 3 && args[0].Equals( "GetCertKeyContainer", StringComparison.InvariantCultureIgnoreCase ) )
        {
            CertKeyContainer( args[1], args[2] );
        }
        else
        {
            Console.WriteLine( usageString );
            Environment.Exit( 1 );
        }
    }

    static void CertThumbprint( string storeName, string certFileOrSubject )
    {
        if ( String.IsNullOrEmpty( storeName ) )
        {
            throw new ArgumentException( "storeName" );
        }
        if ( String.IsNullOrEmpty( certFileOrSubject ) )
        {
            throw new ArgumentException( "certFileOrSubject" );
        }

        try
        {
            X509Certificate2 certificate = GetCertificateFromSubject( storeName, certFileOrSubject );
            if ( certificate == null )
            {
                certificate = GetCertificateFromFileName( certFileOrSubject );                
            }

            if ( certificate != null )
            {
                Console.WriteLine( certificate.Thumbprint );
            }
            else
            {
                Environment.Exit( 1 );
            }
        }
        catch ( Exception e )
        {
            Console.WriteLine( "Encountered an error when attempting to locate certificate thumbprint." );
            Console.WriteLine( "Error Detail:" );
            Console.WriteLine( e.Message );
            Environment.Exit( 1 );
        }
    }

    static void CertKeyContainer( string storeName, string thumbprint )
    {
        if ( String.IsNullOrEmpty( thumbprint ) )
        {
            throw new ArgumentException( "thumbprint" );
        }

        try
        {
            Console.WriteLine( GetCertificateKeyFile( storeName, StoreLocation.LocalMachine, thumbprint ) );
        }
        catch ( Exception e )
        {
            Console.WriteLine( "Encountered an error when attempting to locate certificate key container." );
            Console.WriteLine( "Error Detail:" );
            Console.WriteLine( e.Message );
            Environment.Exit( 1 );
        }
    }

    static void SetSslCert( string requestedCertHashString )
    {
        if ( String.IsNullOrEmpty( requestedCertHashString ) )
        {
            throw new ArgumentException( "requestedCertHashString" );
        }
        try
        {
            ServerManager serverManager = new ServerManager();
            Site defaultWebSite = serverManager.Sites["Default Web Site"];
            if ( defaultWebSite == null )
            {
                throw new Exception( "Unable to obtain a reference to the Default Web Site." );
            }
            if ( defaultWebSite.Bindings == null )
            {
                throw new Exception( "The binding collection on the Default Web Site was null." );
            }
            Binding sslBinding = null;
            foreach ( Binding binding in defaultWebSite.Bindings )
            {
                if ( binding.Protocol.Equals( "https", StringComparison.InvariantCultureIgnoreCase ) )
                {
                    sslBinding = binding;
                    break;
                }
            }
            if ( sslBinding == null )
            {
                Console.WriteLine( "No SSL binding found on the Default Web Site. Creating one..." );
                defaultWebSite.Bindings.Add( "*:443:", "https" );
                foreach ( Binding binding in defaultWebSite.Bindings )
                {
                    if ( binding.Protocol.Equals( "https", StringComparison.InvariantCultureIgnoreCase ) )
                    {
                        sslBinding = binding;
                        break;
                    }
                }
            }
            if ( sslBinding == null )
            {
                throw new Exception( "Unable to get sslBinding" );
            }

            Console.WriteLine( "Setting up the IIS 7 SSL Certificate configuration..." );
            sslBinding.CertificateHash = HexStringToByteArray( requestedCertHashString );
            sslBinding.CertificateStoreName = "MY";
            serverManager.CommitChanges();
        }
        catch ( Exception e )
        {
            Console.WriteLine( "Encountered an error when attempting to set up the IIS 7 SSL Certificate configuration." );
            Console.WriteLine( "Error Detail:" );
            Console.WriteLine( e.Message );
            Environment.Exit( 1 );
        }

    }

    private static byte[] HexStringToByteArray( string requestedCertHashString )
    {
        if ( String.IsNullOrEmpty( requestedCertHashString ) )
        {
            throw new ArgumentException( "requestedCertHashString" );
        }
        int hexLength = requestedCertHashString.Length;
        if ( hexLength % 2 != 0 )
        {
            throw new Exception( requestedCertHashString + " does not have an even number of characters." );
        }
        byte[] certBytes = new byte[hexLength / 2];
        for ( int i = 0; i < hexLength / 2; i++ )
        {
            certBytes[i] = byte.Parse( requestedCertHashString.Substring( 2 * i, 2 ), System.Globalization.NumberStyles.HexNumber );
        }
        return certBytes;
    }

    static void DeleteSslCert( string requestedCertHashString )
    {
        if ( String.IsNullOrEmpty( requestedCertHashString ) )
        {
            throw new ArgumentException( "requestedCertHashString" );
        }
        try
        {
            ServerManager serverManager = new ServerManager();
            Site defaultWebSite = serverManager.Sites["Default Web Site"];
            if ( defaultWebSite == null )
            {
                throw new Exception( "Unable to obtain a reference to the Default Web Site." );
            }
            if ( defaultWebSite.Bindings == null )
            {
                throw new Exception( "The binding collection on the Default Web Site was null." );
            }
            Binding sslBinding = null;
            foreach ( Binding binding in defaultWebSite.Bindings )
            {
                if ( binding.Protocol.Equals( "https", StringComparison.InvariantCultureIgnoreCase ) )
                {
                    sslBinding = binding;
                    break;
                }
            }
            if ( sslBinding == null )
            {
                throw new Exception( "No SSL binding found on the Default Web Site." );
            }
            byte[] existingCertHash = sslBinding.CertificateHash;

            // if no existing ssl cert is found, exit
            if ( existingCertHash == null )
            {
                Console.WriteLine( "WARNING: Site does not have any certificate registered." );
                Console.WriteLine( "Nothing has changed for the SSL configuration." );
                Environment.Exit( 1 );
            }

            string existingCertHashString = BitConverter.ToString( existingCertHash ).Replace( "-", "" );

            // if existing ssl cert does not match requested cert, exit
            if ( !requestedCertHashString.Equals( existingCertHashString, StringComparison.InvariantCultureIgnoreCase ) )
            {
                Console.WriteLine( "WARNING: Site already has a different certificate registered" );
                Console.WriteLine( "Existing Certificate hash: " + existingCertHashString );
                Console.WriteLine( "Requested Certificate hash: " + requestedCertHashString );
                Console.WriteLine( "Nothing has changed for the SSL configuration." );
                Environment.Exit( 1 );
            }

            // we have a match. clear the ssl cert configuration
            Console.WriteLine( "Deleting the IIS 7 SSL Certificate configuration..." );
            sslBinding.CertificateHash = null;
            sslBinding.CertificateStoreName = String.Empty;
            serverManager.CommitChanges();
        }
        catch ( Exception e )
        {
            Console.WriteLine( "Encountered an error when attempting to delete the IIS 7 SSL Certificate configuration." );
            Console.WriteLine( "Error Detail:" );
            Console.WriteLine( e.Message );
            Environment.Exit( 1 );
        }
    }

    /// <summary>
    /// Obtains a certificate from a CER file.
    /// </summary>
    /// <param name="fileName">File to read.</param>
    static X509Certificate2 GetCertificateFromFileName( string fileName )
    {
        X509Certificate2 cert = null;
        try
        {
            cert = new X509Certificate2( fileName );
        }
        catch ( CryptographicException )
        {
        }

        return cert;
    }

     /// <summary>
    /// Obtains a certificate from a given store and subject.
    /// </summary>
    /// <param name="storeName">Store name to look in.</param>
    /// <param name="subject">Subject name.</param>
    /// <returns></returns>
    static X509Certificate2 GetCertificateFromSubject( string storeName, string subject )
    {
        X509Store store = null;
        X509Certificate2 cert = null;
        X509Certificate2Collection certificates = null;
        try
        {
            store = new X509Store( storeName, StoreLocation.LocalMachine );
            store.Open( OpenFlags.ReadOnly );
            certificates = store.Certificates.Find( X509FindType.FindBySubjectDistinguishedName, "CN=" + subject, true );
            if ( certificates.Count > 0 )
            {
                cert = new X509Certificate2( certificates[0] );
            }
        }
        finally
        {
            if ( certificates != null )
            {
                for ( int i = 0; i < certificates.Count; i++ )
                {
                    X509Certificate2 certificate = certificates[i];
                    certificate.Reset();
                }
            }
            if ( store != null )
            {
                store.Close();
            }
        }

        return cert;
    }

    /// <summary>
    /// Get the certificate's private key file name.
    /// </summary>
    /// <param name="name">Certificate store</param>
    /// <param name="location">Certificate location</param>
    /// <param name="subjectName">Certificate SubjectDistinguishedName</param>
    /// <returns></returns>
    static string GetCertificateKeyFile( string storeName, StoreLocation storeLocation, string thumbprint )
    {
        X509Certificate2 certificate = GetCertificate( storeName, storeLocation, thumbprint, true );
        if ( certificate != null )
        {
            return GetKeyFileName( certificate );
        }

        Console.WriteLine( "Can't locate key container." );
        Environment.Exit( 1 );
        return null;
    }

    /// <summary>
    /// Get the ceritificate with the specified SubjectDistinguishedName from the store.
    /// </summary>
    /// <param name="name">Certificate store name.</param>
    /// <param name="location">Certificate store location.</param>
    /// <param name="subjectName">Certificate SubjectDistinguishedName.</param>
    /// <param name="validOnly">true to allow only valid certificates to be returned from the search; otherwise, false.</param>
    /// <returns>X509Certificate2 certificate object. null if the certificate could not be located in store.</returns>
    static X509Certificate2 GetCertificate( string storeName, StoreLocation storeLocation, string thumbprint, bool validOnly )
    {
        X509Store store = new X509Store( storeName, storeLocation );
        X509Certificate2Collection certificates = null;
        store.Open( OpenFlags.ReadOnly );

        try
        {
            X509Certificate2 result = null;

            certificates = store.Certificates.Find( X509FindType.FindByThumbprint, thumbprint, validOnly );

            if ( certificates.Count > 0 )
            {
                // always return the first cert found
                result = new X509Certificate2( certificates[0] );
            }

            return result;
        }
        finally
        {
            if ( certificates != null )
            {
                for ( int i = 0; i < certificates.Count; i++ )
                {
                    X509Certificate2 certificate = certificates[i];
                    certificate.Reset();
                }
            }
            if ( store != null )
            {
                store.Close();
            }
        }
    }

    /// <summary>
    /// Gets the certificate private key file name.
    /// </summary>
    /// <param name="cert">Certificate object</param>
    /// <returns>Private key file name</returns>
    static string GetKeyFileName( X509Certificate2 certificate )
    {
        string filename = null;

        if ( certificate.PrivateKey != null )
        {
            RSACryptoServiceProvider provider = certificate.PrivateKey as RSACryptoServiceProvider;
            filename = provider.CspKeyContainerInfo.UniqueKeyContainerName;
        }
        return filename;
    }

}