Monday, February 11, 2019

Step by Step: Impersonation for WCF SOAP and REST

Hi,

Welcome to my blog. In this post we will see how to achieve Impersonation in WCF ( Windows Communication Foundation). In this we will see both WCF SOAP and REST services.

Overview:
.NET Remoting provides Impersonation easily which helps us in passing clients credentials to server side. In this article we will see how we can achieve impersonation in WCF for both SOAP and REST calls. I tried a lot of  online resources basically everybody said same and no matter what I tried they did not. They just post the code and say it worked. The challenges increased when I was using REST calls. So here I want to have the code ( mostly configuration ) that I used to solve Impersonation issue at one place so that needy can use this.

Introduction:
Simply to say impersonation is a technique that is used to pass client credentials ( like username ) to server side. Normally, clients call a service to perform some action by server on client’s behalf. This article is not an exhaustive tutorial about developing WCF services and calling them. It assumes that the reader has some basic knowledge about WCF creation and calling service methods. This article is written for C# code.

Now let's start our work
For this article, I am using the following tools/technologies to develop a my sample application
Visual Studio 2015
C#
.NET Framework 4.5

SOAP:
First lets see how to achieve impersonation in WCF SOAP service.

WCF Service:
Create a solution and Add new WCF service library project
1. Add using System.Security.Principal at top in service implementation file.
2.Set the impersonation required on the operation implementation of the specific operations as follows:

[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public void GetData()
{
  // code goes here
}

Service Host Configuration:
In service host application configuration file you need to have bindings and use those bindings in service endpoints like below. In below we have basicHttpBinding and wsHttpBinding bindings
<basicHttpBinding>
  <binding name="basicHttpBindingConfiguration" maxBufferSize="2147483647"
           maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647">
     <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                  maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                  maxNameTableCharCount="2147483647" />         
     <security mode="TransportCredentialOnly">
         <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
          <message clientCredentialType="UserName" algorithmSuite="Default" />
      </security>
   </binding>
</basicHttpBinding>
     
<wsHttpBinding>
   <binding name="wsHttpBindingConfiguration"  maxBufferPoolSize="2147483647"
            maxReceivedMessageSize="2147483647" messageEncoding="Text" textEncoding="utf-8"
            useDefaultWebProxy="true" allowCookies="false">
      <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                    maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                    maxNameTableCharCount="2147483647"></readerQuotas>
       <reliableSession ordered="true" inactivityTimeout="00:20:00" enabled="false"></reliableSession>
          <security mode="Message">
            <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
            <message clientCredentialType="Windows" negotiateServiceCredential="true" algorithmSuite="Default" establishSecurityContext="true" />
          </security>
    </binding>
</wsHttpBinding>


Next use above binding in service endpoints
<service behaviorConfiguration="documentWCFServiceBehavior"
         name="AMF.Document.Server.WCF.Service.DocumentWCFService">
  <endpoint address="/basic" binding="basicHttpBinding" bindingConfiguration="basicHttpBindingConfiguration"
            name="basicHttpEndpoint" contract="AMF.Document.Server.WCF.Service.IDocumentWCFService" />
  <endpoint address="/ws" binding="wsHttpBinding" bindingConfiguration="wsHttpBindingConfiguration"
            name="wsHttpEndpoint" contract="AMF.Document.Server.WCF.Service.IDocumentWCFService" />
   <endpoint address="mex" binding="mexHttpBinding" name="mexEndpoint" contract="IMetadataExchange" />
   <host>
      <baseAddresses>
         <add baseAddress="http://localhost:2022/DocumentWCFService" />
       </baseAddresses>
   </host>
</service>


Please see below for full configuration file for WCF service host.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
 
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="basicHttpBindingConfiguration" maxBufferSize="2147483647" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647" />         
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
            <message clientCredentialType="UserName" algorithmSuite="Default" />
          </security>
        </binding>
      </basicHttpBinding>
     
      <wsHttpBinding>
        <binding name="wsHttpBindingConfiguration"  maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647"
                 messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647"></readerQuotas>
          <reliableSession ordered="true" inactivityTimeout="00:20:00" enabled="false"></reliableSession>
          <security mode="Message">
            <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
            <message clientCredentialType="Windows" negotiateServiceCredential="true" algorithmSuite="Default" establishSecurityContext="true" />
          </security>
        </binding>
      </wsHttpBinding>      
    </bindings>
   
    <behaviors>
      <serviceBehaviors>      
        <behavior name="defaultServiceBehavior">
          <serviceMetadata  httpGetEnabled="true" httpGetUrl="" httpsGetEnabled="false"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>      
    </behaviors>

    <services>
      <service behaviorConfiguration="defaultServiceBehavior" name="MyService">
        <endpoint address="/basic" binding="basicHttpBinding" bindingConfiguration="basicHttpBindingConfiguration"
          name="basicHttpEndpoint" contract="IMyService" />
        <endpoint address="/ws" binding="wsHttpBinding" bindingConfiguration="wsHttpBindingConfiguration"
          name="wsHttpEndpoint" contract="IMyService">        
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" name="mexEndpoint"
          contract="IMetadataExchange" />
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000/MyService" />
          </baseAddresses>
        </host>
      </service>    
    </services>

  </system.serviceModel>

</configuration>
Client:
This article doesnot cover how add SOAP service reference. This article assumes reader already has knowledge of how to do it. Once you add servie reference at client, the client's configuration file is updated with service information. Make sure you have security information like below
<bindings>
    <basicHttpBinding>
        <binding name="basicHttpEndpoint" maxBufferSize="2147483647" maxBufferPoolSize="2147483647"
                 maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                             maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                             maxNameTableCharCount="2147483647" />
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
    </basicHttpBinding>
    <wsHttpBinding>
        <binding name="wsHttpEndpoint" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647"
                 messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647"></readerQuotas>
        </binding>
    </wsHttpBinding>
</bindings>


The configuration file be like below

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="basicHttpEndpoint" maxBufferSize="2147483647" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                             maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                             maxNameTableCharCount="2147483647" />
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows" />
          </security>
        </binding>
      </basicHttpBinding>
      <wsHttpBinding>
        <binding name="wsHttpEndpoint" maxBufferPoolSize="2147483647" maxReceivedMessageSize="2147483647"
                 messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
          <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647"></readerQuotas>
        </binding>
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost:8000/MyService/basic"
        binding="basicHttpBinding" bindingConfiguration="basicHttpEndpoint"
        contract="IMyService" name="basicHttpEndpoint" />
      <endpoint address="http://localhost:8000/MyService/ws"
        binding="wsHttpBinding" bindingConfiguration="wsHttpEndpoint"
        contract="IMyService" name="wsHttpEndpoint">       
      </endpoint>
    </client>
  </system.serviceModel>


  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
  </startup>
</configuration>

Below is sample code to call service methods

// endpoint configurations. See client app.config
string configurationName = "basicHttpEndpoint";
//configurationName = "wsHttpEndpoint";

//MyServiceRef is service reference name
//create service client
MyServiceRef.MyServiceClient client = new MyServiceRef.MyServiceClient(configurationName);

//set impersonation level
client.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
//call service method
client.GetData();


REST:
Now lets see how to achieve impersonation in Restful WCF service.
WCF Service:
Create a solution and Add new WCF service library project
1. Add using System.Security.Principal at top in service implementation file.
2.Set the impersonation required on the operation implementation of the specific operations as follows:

[OperationBehavior(Impersonation = ImpersonationOption.Required)]
public void GetData()
{
  // code goes here
}

Interface for above will be like below
[Description("Gets all unprinted items.")]
[OperationContract]       
[WebInvoke(
    Method = "POST",
    UriTemplate = "/getdata",
    BodyStyle = WebMessageBodyStyle.Bare,
    RequestFormat = WebMessageFormat.Json,
    ResponseFormat = WebMessageFormat.Json
    )]
void GetData();


Service Host Configuartion:
In service host application configuration file you need to have bindings and use those bindings in service endpoints like below

<webHttpBinding>
    <binding name="webHttpBindingConfiguration" maxBufferSize="2147483647" maxBufferPoolSize="2147483647"
             maxReceivedMessageSize="2147483647">
         <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647" />
         <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows"/>           
         </security>
     </binding>
</webHttpBinding>


Next use above binding in service endpoints
<service behaviorConfiguration="MyRESTServiceBehavior"
        name="MyRESTService">
  <endpoint address=""
            behaviorConfiguration="webBehaviorConfiguration" binding="webHttpBinding"
            bindingConfiguration="webHttpBindingConfiguration"
           contract="IMyRESTService" />
   <host>
      <baseAddresses>
          <add baseAddress="http://localhost:8000/IMyRESTService" />
       </baseAddresses>
    </host>
    <endpoint address="mex" binding="webHttpBinding" contract="IMetadataExchange"
              bindingConfiguration="webHttpBindingConfiguration" />
</service>


Please see below for full configuration file for WCF service host.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
 
  <system.serviceModel>
    <bindings>     
      <webHttpBinding>
        <binding name="webHttpBindingConfiguration" maxBufferSize="2147483647" maxBufferPoolSize="2147483647"
             maxReceivedMessageSize="2147483647">
         <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647"
                        maxArrayLength="2147483647" maxBytesPerRead="2147483647"
                        maxNameTableCharCount="2147483647" />
          <security mode="TransportCredentialOnly">
            <transport clientCredentialType="Windows"/>           
          </security>
        </binding>
     </webHttpBinding>
    </bindings>
   
    <behaviors>
      <serviceBehaviors>            
        <behavior name="myRESTServiceBehavior">
          <!-- To avoid disclosing metadata information,
          set the values below to false before deployment -->         
          <serviceMetadata  httpGetEnabled="true" httpGetUrl="" httpsGetEnabled="false"/>
          <!-- To receive exception details in faults for debugging purposes,
          set the value below to true.  Set to false before deployment
          to avoid disclosing exception information -->
          <serviceDebug includeExceptionDetailInFaults="False" />
          <!--<serviceCredentials>
            <windowsAuthentication allowAnonymousLogons="false" includeWindowsGroups="true"/>
          </serviceCredentials>-->
        </behavior>
       
      <endpointBehaviors>
        <behavior name="webBehaviorConfiguration">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>

    <services>
      <service behaviorConfiguration="MyRESTServiceBehavior"
        name="MyRESTService">
        <endpoint address=""
            behaviorConfiguration="webBehaviorConfiguration" binding="webHttpBinding"
            bindingConfiguration="webHttpBindingConfiguration"
           contract="IMyRESTService" />
        <host>
         <baseAddresses>
           <add baseAddress="http://localhost:8000/IMyRESTService" />
         </baseAddresses>
        </host>
        <endpoint address="mex" binding="webHttpBinding" contract="IMetadataExchange"
              bindingConfiguration="webHttpBindingConfiguration" />
      </service>
   </services>

  </system.serviceModel>

</configuration>

Client:
Rest service will not have any service references. The RESTful service methods can be called like below

string url = "http://localhost:8000/IMyRESTService/getdata";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.UseDefaultCredentials = true;
request.Method = "GET";
request.ContentType = "application/json";
System.Security.Principal.WindowsIdentity windowsIdentity = System.Security.Principal.WindowsIdentity.GetCurrent();
using(windowsIdentity.Impersonate())
{
   request.ImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
   WebResponse response = request.GetResponse();
   Stream respStream = response.GetResponseStream();
   StreamReader reader = new StreamReader(respStream);
   string responseData = reader.ReadToEnd();
}


Conclusion:
In this article how we can implement impersonation for WCF service for both SOAP and REST calls.
We saw what entries we need to have in configuration files of service host and client applications and how to set impersonation level and call service methods.

I hope you enjoyed reading this post as much as I did while writing it. Please feel free to let me know your valuable and constructive comments and suggestions which can help me to make this article better and improve my technical and written skills. Last but not the least please excuse me for my grammar and typos.

Thanks and have a nice and wonderful day.