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: .NET Framework v2.0, C# 2.0