Saturday, August 26, 2006 11:44 PM bart

.NET Remoting with Windows authentication

The problem statement

An application is running as a service with the (fictional) identity BACH\svcuser and hosts a .NET Remoting type which is published in SingleCall mode. Users of the client application are logged on to the same domain and are authenticated using their own account (e.g. BACH\bart). Calls on the .NET Remoting service should execute under the client user's identity, not the service's identity.

A solution layout

This post is the ideal opportunity to look at a good solution layout for a .NET Remoting based solution using a "service interface" shared by the server and the client. To establish this layout, create three projects:

  • A class library project, called ServiceType, which contains an interface (IDemoService) describing the service (see further).
  • A console application, called Server, to act as a .NET Remoting application hosting the service that implements the service interface (see further). Add a reference to the ServiceType project and to System.Runtime.Remoting.dll.
  • A console application, called Client, to act as a client application for the .NET Remoting service (see further). Add a reference to the ServiceType project and to System.Runtime.Remoting.dll.

The solution should now look as follows:

Service type

The demo service type interface definition is straightforward:

using System;

namespace
ServiceType
{
   public interface
IDemoService
   {
      string
GetIdentity();
   }
}

Server implementation

On to the server implementation which consists of a console application entrypoint (for the sake of the demo; in reality I'm using a Windows Service) and a service type:

using System;
using
ServiceType;
using
System.Security.Principal;
using
System.Runtime.Remoting.Channels.Tcp;
using
System.Runtime.Remoting.Channels;
using
System.Runtime.Remoting;
using
System.Threading;

namespace
Server
{
   class
Program
   {
      static void Main(string
[] args)
      {
         TcpChannel channel = new TcpChannel
(2468);
         ChannelServices.RegisterChannel(channel, true
);

         RemotingConfiguration.RegisterWellKnownServiceType(typeof(DemoService), "demoservice", WellKnownObjectMode.SingleCall);

         Console.WriteLine("Service running as {0}...", WindowsIdentity
.GetCurrent().Name);
         Console
.ReadLine();
      }
   }

   public class DemoService : MarshalByRefObject,
IDemoService
   {
      public string
GetIdentity()
      {
         WindowsIdentity identity = Thread
.CurrentPrincipal.Identity as WindowsIdentity;
         if
(identity != null && identity.IsAuthenticated)
           
return
identity.Name;
         else
            return null
;
      }
   }
}

The DemoService class is the service type. Therefore it derives from MarshalByRefObject and furthermore we implement the IDemoService interface defined in a separate project. The GetIdentity method implementation is pretty straightforward.

The Program class contains the entry point of the application and registers the service on tcp://localhost:2468/demoservice with the SingleCall activation mode (i.e. an object of type DemoService gets created for each method call and is destroyed afterwards, comparable to a stateless web service).

Client implementation

The client's implementation is also fairly easy for .NET Remoting fans:

using System;
using
ServiceType;
using
System.Runtime.Remoting.Channels.Tcp;
using
System.Runtime.Remoting.Channels;
using
System.Security.Principal;

namespace
Client
{
   class
Program
   {
      static void Main(string
[] args)
      {
         TcpChannel channel = new TcpChannel
();
         ChannelServices.RegisterChannel(channel, true
);

         IDemoService svc = (IDemoService) Activator.GetObject(typeof(IDemoService), "tcp://localhost:2468/demoservice"
);

         Console
.WriteLine("Client running as {0}...", WindowsIdentity.GetCurrent().Name);
         Console.WriteLine("Thread identity on the server: {0}"
, svc.GetIdentity());
         Console
.ReadLine();
      }
   }
}

Testing it

Start the server executable (server.exe) in one console window:

Start the client executable (client.exe) in another console window:

Nothing special so far. Now run the client executable (client.exe) as another user using the runas command runas client.exe /user:test:

That's exactly what we desired.

The trick?

The trick is simple but a bit underdocumented. First of all, since .NET 2.0 the TcpChannel (as well as the HttpChannel) supports SSPI as mentioned on MSDN. Furthermore there is a new RegisterChannel overload on the ChannelServices class that takes a boolean second parameter called "ensureSecurity". By turning this on (on both client and server) SSPI seems to work fine across the wire. Notice the one-parameter RegisterChannel method is marked as deprecated as of .NET 2.0. The documentation is rather simplistic:

If the ensureSecurity parameter is set to true, the remoting system determines whether the channel implements ISecurableChannel, and if so, enables encryption and digital signatures. An exception is thrown if the channel does not implement ISecurableChannel.

But as you can see, setting the flag does the trick.

Taking it one step further

The application I'm working on requires a little more. As the matter in fact, it's a server with two faces. One face is the management face. Its goal is for users to send commands to the server which are then dispatched to multiple other machines (the second face, aka dispatching interface). In order to be eligible to send such a command, the management face requires end-user authentication and authorizes the user. If the user is permitted to send the command, the dispatching face kicks in and dispatches the command to the target machines. This time, the end user's identity should not be forwarded, but the dispatched command should be running as the service user. Looks a little complex? A little example will help:

Assume that the server (SERVER) is running as BACH\svcuser and is waiting for commands to come in through the management face. Now the following happens:

  • Management client PCMGMT runs as BACH\Bart and sends SayHello(new string[] { "PC01", "PC02" }) to SERVER.
  • The server has received the SayHello message on the management face. The thread doing the work runs as BACH\Bart (not BACH\svcuser) thanks to SSPI ("impersonation").
    • BACH\Bart is authorized and is confirmed to be eligible to send the SayHello command.
    • The dispatching face of the server sends a Hello message to PC01 acting as BACH\svcuser (not BACH\Bart).
      • PC01 receives the Hello message on a thread running as BACH\svcuser (similar to PCMGMT-to-SERVER as BACH\Bart but now SERVER-to-PC01 as BACH\svcuser).
    • The dispatching face of the server sends a Hello message to PC02 acting as BACH\svcuser (not BACH\Bart).
      • PC02 receives the Hello message on a thread running as BACH\svcuser (similar to PCMGMT-to-SERVER as BACH\Bart but now SERVER-to-PC02 as BACH\svcuser).

To do this, we can use the class WindowsImpersonationContext as shown below:

using (WindowsImpersonationContext ctx = svcUser.Impersonate())
{
   // Do work acting as the service user
}

In this piece of code the svcUser object is of type WindowsIdentity and refers to the original identity the service was started as. Let's show a more complete example (changes indicated in bold).

Service type

using System;

namespace
ServiceType
{
   public interface
IDemoService
   {
      string
GetIdentity();
      void SomeOperation();
   }
}

Server implementation

using System;
using
ServiceType;
using
System.Security.Principal;
using
System.Runtime.Remoting.Channels.Tcp;
using
System.Runtime.Remoting.Channels;
using
System.Runtime.Remoting;
using
System.Threading;

namespace
Server
{
   class
Program
   {
      public static WindowsIdentity Identity;

      static void Main(string
[] args)
      {
         TcpChannel channel = new TcpChannel
(2468);
         ChannelServices.RegisterChannel(channel, true
);

         RemotingConfiguration.RegisterWellKnownServiceType(typeof(DemoService), "demoservice", WellKnownObjectMode.SingleCall);

         Identity = WindowsIdentity.GetCurrent();

         Console.WriteLine("Service running as {0}...",
Identity.Name);
         Console
.ReadLine();
      }
   }

   public class DemoService : MarshalByRefObject,
IDemoService
   {
      public string
GetIdentity()
      {
         WindowsIdentity identity = Thread
.CurrentPrincipal.Identity as WindowsIdentity;
         if
(identity != null && identity.IsAuthenticated)
           
return
identity.Name;
         else
            return null
;
      }
   }

   public void SomeOperation()
   {
      WindowsIdentity identity = (WindowsIdentity)Thread
.CurrentPrincipal.Identity;

      using (WindowsImpersonationContext
ctx = identity.Impersonate())
      {
         // Here we are impersonating as the management client user
         Console.WriteLine(WindowsIdentity
.GetCurrent().Name);
      }

      using (WindowsImpersonationContext ctx = Program
.Identity.Impersonate())
      {
         // Do work acting as the service user
         Console.WriteLine(WindowsIdentity
.GetCurrent().Name);
      }

      using (WindowsImpersonationContext ctx = identity.Impersonate())
      {
         // Here we are impersonating as the management client user
         Console.WriteLine(WindowsIdentity
.GetCurrent().Name);
      }
   }
}

Client implementation

The client's implementation is also fairly easy for .NET Remoting fans:

using System;
using
ServiceType;
using
System.Runtime.Remoting.Channels.Tcp;
using
System.Runtime.Remoting.Channels;
using
System.Security.Principal;

namespace
Client
{
   class
Program
   {
      static void Main(string
[] args)
      {
         TcpChannel channel = new TcpChannel
();
         ChannelServices.RegisterChannel(channel, true
);

         IDemoService svc = (IDemoService) Activator.GetObject(typeof(IDemoService), "tcp://localhost:2468/demoservice"
);

         Console
.WriteLine("Client running as {0}...", WindowsIdentity.GetCurrent().Name);
         Console.WriteLine("Thread identity on the server: {0}"
, svc.GetIdentity());
         svc.SomeOperation();
         Console
.ReadLine();
      }
   }
}

The result on the server when running the server.exe as VISTA-9400\Bart and the client.exe as VISTA-9400\test:

Happy coding!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

# re: .NET Remoting with Windows authentication

Wednesday, September 27, 2006 6:52 PM by Ben

Excellent article.  Thank you.

When I change the tcp channel to an http channel,

   ChannelServices.RegisterChannel(httpChannel, true);
   HttpChannel httpChannel = new HttpChannel(9888);

the code throws an exception.  

"Host the service in IIS with Integrated Windows Authentications to secure the server".

Our application is a windows service and currently uses an HTTP channel in 1.1 without being hosted in IIS.   We would love to upgrade to 2.0 and secure the channel but don't want to require IIS.   Is this possible?


# re: .NET Remoting with Windows authentication

Wednesday, September 27, 2006 7:16 PM by bart

Hi Ben,

I didn't try the HTTP channel for this particular scenario yet but it looks pretty justified to require IIS with Integrated Windows Authentication to support the authentication. If you don't use the security flag (second parameter of RegisterChannel set to false), it should work fine I think.

-Bart