26 March, 2014

WCF impersonation with reusable proxy

Windows Communication Foundation (WCF) is a powerful framework for building distributed service oriented applications. Services offer resources in the specific domain which are accessed by the clients. In general clients call a service to have the service perform some action on the client’s behalf - this is an impersonation. It allows the service to act as the client while performing the action.  To be more specific - the service runs in the client security context (e.g. service thread is executed in a security context that is different from the process that owns the thread). As shown in the picture below the impersonation is used to deal with credentials on the same machine Computer A:
Impersonation

Delegation is a special way of flowing impersonation using client security context to the  back-end service (e.g. impersonation chain along services). Impersonation is used to access resources on the same machine as the service, while delegation is used to access resources that are remote to the initial service. The picture below shows the delegation is used to delegate credentials between different machines Computer A and Computer B:
Delegation

WCF offers three impersonation options for WCF service operations via enumeration type System.ServiceModel.ImpersonationOption:

  1. NotAllowed: impersonation is not performed on particular service operation.

  2. Allowed: impersonation is performed when caller Windows identity token (credentials) is available and the service is configured to impersonate on all operations using ImpersonateCallerForAllOperations in WCF configuration section ServiceAuthorizationBehavior

  1. Required: impersonation is required and the caller Windows identity token has to be available unless an exception will occur.


By default, WCF applies NotAllowed impersonation option for all operations. In order to enable it, there are two possibilities:


  1. Setting OperationBehavior attribute to be set to Required at specific operation. This attribute cannot be applied on contract but only at the actual operation’s implementation.  [OperationBehavior(Impersonation = ImpersonationOption.Required)]
    public string Ping() {
  ....
      }

 
  1. Setting ImpersonateCallerForAllOperations property to true in the Service Authorization Behavior in configuration section. This implies impersonation at service level for all its operations.
...
<behaviors>
 <serviceBehaviors>
   <behavior name="ServiceBehavior">
     <serviceAuthorization impersonateCallerForAllOperations="true" />
   </behavior>
 </serviceBehaviors>
</behaviors>
...


In the next example we see how to obtain the caller (client) identity token inside the  service operation. To be more generic we use abstract class for getting client token information using System.ServiceModel.ServiceSecurityContext class:


namespace EE.Framework.Services {
 public abstract class BaseService {
   ...
   protected string CallerUserName {
     get { return this.CallerIdentity.Name ?? null; }
   }


   protected WindowsIdentity CallerIdentity {
     get { return ServiceSecurityContext.Current.WindowsIdentity; }
   }
   ...
 }
}


In the service class we can get information about the client credentials. The service contract IPingContract is the following:


namespace EE.Framework.ServiceContracts {


 [ServiceContract(Namespace = "http://myuri/services/ping")]
 public interface IPingContract {
    [OperationContract()]
    string Ping();
 }
}


The service class PingService is defined as:


namespace EE.Framework.Services {

 public class PingService: BaseService, IPingContract {

    [OperationBehavior(Impersonation = ImpersonationOption.Required)]

    public string Ping() {
      return string.Format("Reply from {0} @ {1} [caller: {2}].",
              Environment.MachineName, DateTime.Now, base.CallerUserName);
    }
 }
}


The service will be hosted in IIS so the web.config file needs to be modified. As we use OperationBehavior attribute the ImpersonateCallerForAllOperations property is not needed (it is commented):


...
<system.serviceModel>
 <services>
   <!-- PingService: TCP endpoint -->
   <service behaviorConfiguration="PingServiceBehavior"                                  
            name="EE.Framework.Services.PingService">
     <endpoint address="tcp" binding="netTcpBinding"
               contract="EE.Framework.ServiceContracts.IPingContract"
               name="NetTcpBinding_IPingContract"    
               bindingName="NetTcpBinding_IPingContract" />
     <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
   </service>
 </services>


 <!-- BINDINGS -->
 <bindings>
   <netTcpBinding>
     <binding  name="NetTcpBinding_IPingContract">
       <security mode="None" />
     </binding>
   </netTcpBinding>
 </bindings>


 <!-- BEHAVIORS -->
 <behaviors>
   <serviceBehaviors>
     <behavior name="PingServiceBehavior">
       <serviceMetadata httpGetEnabled="true" />
        <serviceDebug includeExceptionDetailInFaults="true" />
        <!-- <serviceAuthorization impersonateCallerForAllOperations="true" /> -->
     </behavior>
   </serviceBehaviors>
 </behaviors>
</system.serviceModel>
...

The service file
PingService.svc contains the following markup code:


<%@ ServiceHost Service="EE.Framework.Services.PingService, EE.Framework.Services" %>


The impersonation of the client Windows identity token is driven by enumeration type System.Security.Principal.TokenImpersonationLevel:


  1. None: the impersonation on WCF service is disabled.

  2. Anonymous: the WCF service authenticates client as anonymous user but cannot retrieve information about identification or impersonation.

  3. Identification: the WCF service can authenticate clients but cannot make impersonation. This is default value of the WCF client.

  4. Impersonation: the WCF service can authenticate, retrieve information for identification and impersonate client’s security context on its local system. The WCF service cannot impersonate client on remote system.

  5. Delegation: the WCF service can authenticate, retrieve information for identification and perform impersonation on remote system.



If the WCF client is the web application, we need to enable ASP.NET impersonation (which is disabled by default) and enable Windows authentication (anonymous has to be disabled):


<configuration>
 ...
 <system.web>
   ...
   <identity impersonate="true" />
   <authentication mode="Windows" />
   ...
 </system.web>
 ...
</configuration>


We need to set specific impersonation level as well. This can be done in a two ways:


  1. Configuration file
    <system.serviceModel>
       ...
       <behaviors>
        <endpointBehaviors>
           <behavior>
               <clientCredentials>
                <windows allowedImpersonationLevel="Impersonation" />
               </clientCredentials>
           </behavior>
        </endpointBehaviors>
       </behaviors>
       ...
</system.serviceModel>


  1. Setting impersonation at proxy
using System.Security.Principal;
...
TokenImpersonationLevel til = TokenImpersonationLevel.Impersonation;
PingServiceProxy proxy = new PingServiceProxy();
proxy.ClientCredentials.Windows.AllowedImpersonationLevel = til;
...


When calling service from the client we need to impersonate block of code using WindowsIdentity class:


using System.Security.Principal;
...
TokenImpersonationLevel til = TokenImpersonationLevel.Impersonation;
using (((WindowsIdentity)User.Identity).Impersonate()) {
 PingServiceProxy proxy = new PingServiceProxy();
 proxy.ClientCredentials.Windows.AllowedImpersonationLevel = til;
 string result = proxy.Ping();
 ...
}
...


After that we get the following string result with caller name:


Reply from DEV-PC @ 20.12.2013 13:39:41 [caller: DEV-PC\DAMJANK].


The next step is to use reusable client proxy that can be applied for any WCF service using its contract only.  Brandon Zeider wrote a good article how to create generic service client using WCF ChannelFactory with Castle’s DynamicProxy

Below is an example of service call with generic proxy (endpoint variable contains the name of the service endpoint defined in web.config):


using System.Security.Principal;
...
string endpoint = "NetTcpBinding_IPingContract";
TokenImpersonationLevel til = TokenImpersonationLevel.Impersonation;
using (((WindowsIdentity)User.Identity).Impersonate()) {
 string result = ServiceClient<IPingContract>.CreateProxy(endpoint, til).Ping();
 ...
}
...


There are two classes where we added additional TokenImpersonationLevel parameter - ServiceClient and WcfInterceptor. ServiceClient is a static class that returns a proxy object which implements service contract (interface).


using Castle.DynamicProxy;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Security.Principal;
...


public static class ServiceClient<T> where T : class {
 private static ProxyGenerator generator = new ProxyGenerator();


 public static T CreateProxy() {
   return generator.CreateInterfaceProxyWithoutTarget<T>(new WcfInterceptor<T>());
 }
 
 public static T CreateProxy(string endpointConfigName) {
   return generator.CreateInterfaceProxyWithoutTarget<T>(new
                    WcfInterceptor<T>(endpointConfigName));
 }


 public static T CreateProxy(string endpointConfigName,
                             TokenImpersonationLevel tokenImpersonationLevel) {
   return generator.CreateInterfaceProxyWithoutTarget<T>(new
                    WcfInterceptor<T>(endpointConfigName, tokenImpersonationLevel));
 }


 public static T CreateProxy(Binding binding, EndpointAddress address) {
   return generator.CreateInterfaceProxyWithoutTarget<T>(new
                    WcfInterceptor<T>(binding, address));
 }


 public static T CreateProxy(Binding binding, EndpointAddress address,
                             TokenImpersonationLevel tokenImpersonationLevel) {
   return generator.CreateInterfaceProxyWithoutTarget<T>(new
                    WcfInterceptor<T>(binding, address, tokenImpersonationLevel));
 }
}


WcfInterceptor is internal class instantiated by ServiceClient. It accepts service contract as generic parameter.


using System.ServiceModel;
using System.ServiceModel.Channels;
using Castle.DynamicProxy;
using System.ServiceModel.Description;
using System.Net;
using System.Security.Principal;
...
internal sealed class WcfInterceptor<T> : IInterceptor where T : class {
 private ChannelFactory<T> factory = null;


 public ClientCredentials Credentials {
    get { return this.factory.Credentials; }
 }


 public WcfInterceptor() {
   this.factory = new ChannelFactory<T>();
 }


 public WcfInterceptor(TokenImpersonationLevel til) {
   this.factory = new ChannelFactory<T>();
   this.factory.Credentials.Windows.AllowedImpersonationLevel = til;
 }


 public WcfInterceptor(string endpointConfigName) {
   this.factory = new ChannelFactory<T>(endpointConfigName);
 }


 public WcfInterceptor(string endpointConfigName, TokenImpersonationLevel til) {
   this.factory = new ChannelFactory<T>(endpointConfigName);
   this.factory.Credentials.Windows.AllowedImpersonationLevel = til;
 }


 public WcfInterceptor(Binding binding, EndpointAddress address) {
   this.factory = new ChannelFactory<T>(binding, address);
 }


 public WcfInterceptor(Binding binding, EndpointAddress address,
                       TokenImpersonationLevel til) {
   this.factory = new ChannelFactory<T>(binding, address);
   this.factory.Credentials.Windows.AllowedImpersonationLevel = til;
 }


 public WcfInterceptor(NetworkCredential credential) : this() {
   this.factory.Credentials.Windows.ClientCredential = credential;
 }


 public WcfInterceptor(string endpointConfigName, NetworkCredential credential) :
        this(endpointConfigName) {
   this.factory.Credentials.Windows.ClientCredential = credential;
 }


 public WcfInterceptor(Binding binding, EndpointAddress address,
                       NetworkCredential credential) : this(binding, address) {
   this.factory.Credentials.Windows.ClientCredential = credential;
 }


 public void Intercept(IInvocation invocation) {
   T channel = CreateChannel();
   invocation.ReturnValue = invocation.Method.Invoke(channel, invocation.Arguments);
   CloseChannel(channel);
 }


 private T CreateChannel() {
   return this.factory.CreateChannel();
 }
 
 private void CloseChannel(T channel) {
   if (channel != null) {
     try {
       ICommunicationObject obj = (ICommunicationObject)channel;
         if (obj.State != CommunicationState.Faulted) {
             if (obj.State != CommunicationState.Closed) {
            obj.Close();
             }
         } else {
             obj.Abort();
         }
         Logger.Debug(string.Format("* WcfInterceptor.CloseChannel                                     
                                    (ComState={0})", obj.State));
     } catch {
         try {
             ((ICommunicationObject)channel).Abort();
        } catch { throw; }
     }
   }
 }
}

No comments: