WCF Impersonation and Delegation

WCF Impersonation and Delegation

Old forum URL: forums.lhotka.net/forums/t/8615.aspx


frankhoffy posted on Thursday, March 04, 2010

We have the following setup:

  1. Client (Windows Forms)
  2. IIS Server
  3. Database Server

We have written a CSLA 3.8.0 library and would like to run it remotely via WCF.  We would like to set up the following scenarios:

  1. Authenticate to the database server with the logged-in user's credentials
  2. Authenticate to the database server using a dedicated Active Directory account

We've really been beating our heads against the wall trying to figure out what we need to put in our config files to make this happen though.  When we make the call to our WCF service, it seems to lose the credentials when it tries to connect to the database.  Both servers trust each other for delegation.

If somebody could provide me with a very simple example of how to set up those two authentication scenarios, I would greatly appreciate it.

If not an example, I'd like to at least know if anybody out there has successfully implemented this.  The goal on our end is to be able to do all of this through configuration files, so that we can switch between running locally versus remotely without having to implement code changes.

frankhoffy replied on Monday, March 08, 2010

I feel like I'm getting close but I'm not quite there.  I have a hard time believing nobody has attempted this before.

So far I have added this to the service's web.config file:

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
          <serviceAuthorization impersonateCallerForAllOperations="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service behaviorConfiguration="ServiceBehavior" name="Csla.Server.Hosts.WcfPortal">
        <endpoint binding="wsHttpBinding"
                  contract="Csla.Server.Hosts.IWcfPortal" />
      </service>
    </services>
  </system.serviceModel>

I also added this to the client's app.config:

  <system.serviceModel>
    <behaviors>
      <endpointBehaviors>
        <behavior name="ImpersonationBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Impersonation" />
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>

    <client>
      <endpoint name="WcfDataPortal" behaviorConfiguration="ImpersonationBehavior"
                address="http://mywebserver/DocumentManagementHost/WcfService.svc"
                binding="wsHttpBinding"
                contract="Csla.Server.Hosts.IWcfPortal" />
    </client>
  </system.serviceModel>

What I'm getting back from the server's event log is the following:

WebHost failed to process a request.
 Sender Information: System.ServiceModel.ServiceHostingEnvironment+HostingManager/2685261
 Exception: System.ServiceModel.ServiceActivationException: The service '/DocumentManagementHost/WcfService.svc' cannot be activated due to an exception during compilation.  The exception message is: The service operation 'Create' that belongs to the contract with the 'IWcfPortal' name and the 'http://ws.lhotka.net/WcfDataPortal' namespace does not allow impersonation.. ---> System.InvalidOperationException: The service operation 'Create' that belongs to the contract with the 'IWcfPortal' name and the 'http://ws.lhotka.net/WcfDataPortal' namespace does not allow impersonation.
   at System.ServiceModel.Dispatcher.SecurityValidationBehavior.WindowsIdentitySupportRule.Validate(ServiceDescription description)
   at System.ServiceModel.Dispatcher.SecurityValidationBehavior.System.ServiceModel.Description.IServiceBehavior.Validate(ServiceDescription description, ServiceHostBase serviceHostBase)
   at System.ServiceModel.Description.DispatcherBuilder.ValidateDescription(ServiceDescription description, ServiceHostBase serviceHost)
   at System.ServiceModel.Description.DispatcherBuilder.InitializeServiceHost(ServiceDescription description, ServiceHostBase serviceHost)
   at System.ServiceModel.ServiceHostBase.InitializeRuntime()
   at System.ServiceModel.ServiceHostBase.OnOpen(TimeSpan timeout)
   at System.ServiceModel.Channels.CommunicationObject.Open(TimeSpan timeout)
   at System.ServiceModel.Channels.CommunicationObject.Open()
   at System.ServiceModel.ServiceHostingEnvironment.HostingManager.ActivateService(String normalizedVirtualPath)
   at System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
   --- End of inner exception stack trace ---
   at System.ServiceModel.ServiceHostingEnvironment.HostingManager.EnsureServiceAvailable(String normalizedVirtualPath)
   at System.ServiceModel.ServiceHostingEnvironment.EnsureServiceAvailableFast(String relativeVirtualPath)
 Process Name: w3wp
 Process ID: 8500

For more information, see Help and Support Center at http://go.microsoft.com/fwlink/events.asp.

It looks like if I can allow impersonation for WcfDataPortal somehow, I may be OK.  I'm just not sure how to do that.  I suspect it has something to do with adding [OperationBehavior(Impersonation = ImpersonationOption.Required)] to the service methods.

I'm not sure what sort of settings I would have to use to impersonate a different user as well.

RockfordLhotka replied on Monday, March 08, 2010

You may also need to subclass WcfProxy to configure the underlying WCF proxy object with your client Windows credentials. I don't know if that's required, but I do know we enhanced WcfProxy a long time ago so it was easy to subclass for the purpose of setting things like credentials on the actual WCF objects.

frankhoffy replied on Monday, March 08, 2010

Thank you for the reply Rocky.  I feel like I'm getting much closer to the answer.  I implemented my own custom WcfProxy in my client app.  I tried setting some different values of the ChannelFactory but I kept getting the error I pasted above.  I may not be setting all that I need to be setting though.  There appears to be some provision for logging in as a specific user as well.  I pasted the basic code for the proxy below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Security;

namespace DocumentManagementUI
{
    public class WcfProxy : Csla.DataPortalClient.WcfProxy
    {
        protected override ChannelFactory<Csla.Server.Hosts.IWcfPortal> GetChannelFactory()
        {
            var f = base.GetChannelFactory();
            return f;
        }
    }
}

All I need to figure out now is what goes before "return f".

RockfordLhotka replied on Monday, March 08, 2010

Have you read through the WCF security guidance book from Patterns and Practices?

http://www.codeplex.com/WCFSecurity/

That's probably the most comprehensive source for WCF security configuration available.

frankhoffy replied on Tuesday, March 09, 2010

Thank you Rocky that link was very helpful.  I believe I have put all of the pieces together.  Hopefully this thread will be helpful for anybody attempting this in the future.

First and foremost, I had to make a modification to CSLA to make this happen.  I would like to request this as an enhancement to the framework.  I had to go into the WcfPortal class and decorate all of the methods with the following attribute:

[OperationBehavior(Impersonation = ImpersonationOption.Allowed)]

I confirmed that my configuration would not work without this attribute being defined.  I would hope that this could be added to CSLA without causing any unwanted side effects.

Below I have pasted the complete web.config file for the service (which is hosted in IIS).  This is essentially the same as what I pasted earlier.  In IIS, the web application is configured to allow anonymous as well as windows integrated authentication.  WCF has its own authentication mechanism.  If you don't allow anonymous in IIS, WCF will give you an error.

<?xml version="1.0"?>
<configuration>

  <appSettings>
    <add key="CslaAuthentication" value="Windows"/>
  </appSettings>

  <connectionStrings>
    <add name="DocumentManagement" connectionString="Data Source=mytestserver;Initial Catalog=DocumentManagement;Integrated Security=True" providerName="System.Data.SqlClient"/>
    <add name="Biennium" connectionString="Data Source=mytestserver;Initial Catalog=Atlas;Integrated Security=True" providerName="System.Data.SqlClient"/>
  </connectionStrings>

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="ServiceBehavior">
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="false" />
          <serviceAuthorization impersonateCallerForAllOperations="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service behaviorConfiguration="ServiceBehavior" name="Csla.Server.Hosts.WcfPortal">
        <endpoint binding="wsHttpBinding"
                  contract="Csla.Server.Hosts.IWcfPortal" />
      </service>
    </services>
  </system.serviceModel>
</configuration>

It's up to the client to determine which identity to send to the service.  In the first scenario, I wanted to impersonate the calling user.  To do that, this is how I had to configure the client's app.config.  Note that I had to adjust the maxReceivedMessageSize or I'd get an error about the message being too big.  The kicker here is that I had to set allowedImpersonationLevel to Delegation, not Impersonation.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="CslaAuthentication" value="Windows"/>
    <add key="CslaDataPortalProxy" value="Csla.DataPortalClient.WcfProxy, Csla"/>
  </appSettings>

  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="wsHttpBinding_Limits"
                 maxReceivedMessageSize="2147483647">
        </binding>
      </wsHttpBinding>
    </bindings>
   
    <behaviors>
      <endpointBehaviors>
        <behavior name="ImpersonationBehavior">
          <clientCredentials>
            <windows allowedImpersonationLevel="Delegation" />
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>

    <client>
      <endpoint name="WcfDataPortal" behaviorConfiguration="ImpersonationBehavior"
                address="http://mywebserver/DocumentManagementHost/WcfService.svc"
                binding="wsHttpBinding"
                bindingConfiguration="wsHttpBinding_Limits"
                contract="Csla.Server.Hosts.IWcfPortal" />
    </client>
  </system.serviceModel>
</configuration>

Next I wanted to test impersonating a specific user.  To do this, I had to subclass my own WcfProxy class in the client application.  Here's what that code looks like:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Principal;
using System.ServiceModel;
using System.ServiceModel.Security;

namespace DocumentManagementUI
{
    public class WcfProxy : Csla.DataPortalClient.WcfProxy
    {
        protected override ChannelFactory<Csla.Server.Hosts.IWcfPortal> GetChannelFactory()
        {
            var f = base.GetChannelFactory();
            f.Credentials.Windows.ClientCredential = new System.Net.NetworkCredential("userName", "password", "domain");
            return f;
        }
    }
}

After that was done, I had to go back into the client's app.config and modify the CslaDataPortalProxy value to look like this:

    <add key="CslaDataPortalProxy" value="DocumentManagementUI.WcfProxy, DocumentManagement"/>

That's about it.  Both scenarios are taken care of using this method.  Hopefully my plea to get those attributes added to those CSLA methods goes through so I don't have to keep modifying the library :).

brianbuten replied on Monday, September 26, 2011

I tried the suggestion of adding the attribute tags as suggested and it works, thanks for posting.  Has this been addressed in later releases, or are we supposed to modify the framework?

RockfordLhotka replied on Monday, September 26, 2011

I don't think modifying  the framework is required, because you can create your own subclass of WcfPortal and use that instead.

Copyright (c) Marimer LLC