I have a duplex WCF service over netTcpBinding, the service uses subscription model where clients would subscribe to the service with a callback implementation using Subscribe/Unsubscribe methods.
The service has a custom IAuthorizationPolicy which authenticates users from the database and sets default thread principal to a custom principal in IAuthorizationPolicy.Evaluate() method.
The service is running as a service in IIS 7.5, under LOCALSYSTEM identity, Windows 7
First, here's how client (WinForms) authenticates users:
- Show logon form to ask for credentials
- Call another authentication service on a different endpoint to validate user credentials
- Set default thread principal using AppDomain.SetThreadPrincipal
The problem i'm facing is that in the callback threads, and only in the callback threads, Thread.CurrentPrincipal in the is reset to an anonymous GenericPrincipal object and my custom principal that was set after authentication is not propagated.
UPDATE: The principal is propagated as expected if i set securityMode and clientCredentialType to "None", obviously this have something to do with security configuration, i have tried lots of combination of configuration with no luck.
UPDATE: I have included a sample project reproducing the issue.
The solution is straight forward with everything configured and running it will take just few clicks and few minutes of your precious time, it contains 5 projects:
- PM.Services - The WCF service project
- PM.Contracts - a lib containing the service contract
- Client/Broadcaster - a console application broadcasts to other clients through the service.
- Client/Listener - a console application that subscribes to the service/listens to broadcasts (this is where the Thread.CurrentPrincipal is reset).
- Client/Shared - a lib shared between the two clients that contains the callback implementation and service creation code.
To configure the service on your machine (assuming IIS7 or later already enabled on machine), the solution root folder contains two batch files:
InstallService.bat: This batch will:
Enable the following windows features, and their parent features on your machine (if not already enabled): Windows Communication Foundation HTTP Activation and Windows Communication Foundation Non-HTTP Activation, to enable WCF and net.tcp.
Will create and add service certificate in TrustedPeople store.
Will create a website for the service that will listen on: 7359 (http) and 7357 (net.tcp)
Will create an ApplicationPool under LOCALSYSTEM identity (to avoid permission issues during the testing)
Will create a folder in c:\inetpub\WCFCallbackSample and copy service output to it.
UninstallService.bat: This batch will:
Remove the service certificate from TrustedPeople store.
Uninstall the service website.
Delete the website folder.
Running the sample solution:
Build the solution
Install the service using InstallService.bat.
Debug/Run the solution to run the two service consumers (Client\Broadcaster, Client\Listener)
Send messages from Broadcaster
In the listener you will notice that the callback thread identity (printed on the console) is different from the main thread's identity set by SetThreadPrincipal on application start.
Code from initial post:
Service Interface:
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IFinanceServiceCallback))]
public interface IFinanceService : IServiceContract, ISupportCallback // <-- Interface that defines Subscribe and Unsubscribe methods
{
[OperationContract]
void DuplexMessageTest(string text);
}
Service implementation:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Single)]
public class FinanceService : IFinanceService
{
// Subscription boilerplate code
private static readonly
DuplexServiceSubscriptionManager<IFinanceServiceCallback> _subscriptionManager = new DuplexServiceSubscriptionManager<IFinanceServiceCallback>();
public bool Subscribe()
{
return _subscriptionManager.Subscribe();
}
public bool Unsubscribe()
{
return _subscriptionManager.Unsubscribe();
}
// A test method for simulation
public void DuplexMessageTest(string text)
{
_subscriptionManager.InvokeCallbackMethod(callback => callback.OnDuplexTest(text));
}
Callback implementation:
[CallbackBehavior(UseSynchronizationContext = false, ConcurrencyMode = ConcurrencyMode.Single, AutomaticSessionShutdown=true)]
public class FinanceServiceCallbackSubscription : BaseServiceSubscription<IFinanceService>, IFinanceServiceCallback
{
public FinanceServiceCallbackSubscription() : base()
{
}
public delegate void DuplexTestEventHandler(string message);
public event DuplexTestEventHandler DuplexTest;
public void OnDuplexTest(string message)
{
-->> At this point, the Thread.CurrentPrincipal is reset <<--
if (DuplexTest != null)
DuplexTest(message);
}
}
UPDATE Service Configuration
<system.serviceModel>
<services>
...
<service behaviorConfiguration="PM.Behaviour_01" name="PM.Services.FinanceService">
<endpoint address="" binding="netTcpBinding" bindingConfiguration="PMStandardBindingConfiguration"
contract="PM.Contracts.IFinanceService" />
<endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />
</service>
...
<services/>
<bindings>
...
<netTcpBinding>
<binding name="PMStandardBindingConfiguration"
closeTimeout="00:01:00"
openTimeout="00:01:00"
receiveTimeout="00:10:00"
sendTimeout="00:10:00"
maxReceivedMessageSize="2147483647"
maxBufferPoolSize="0"
transferMode="Buffered"
portSharingEnabled="false">
<readerQuotas maxDepth="2147483647"
maxStringContentLength="2147483647"
maxArrayLength="2147483647"
maxBytesPerRead="2147483647"
maxNameTableCharCount="16384" />
<security mode="Message">
<message clientCredentialType="UserName" />
</security>
<reliableSession ordered="true" inactivityTimeout="10675199.02:48:05.4775807" />
</binding>
</netTcpBinding>
...
<bindings/>
<behaviors>
...
<behavior name="PM.Behaviour_01">
<serviceMetadata httpGetEnabled="false" />
<serviceCredentials>
<userNameAuthentication userNamePasswordValidationMode="Custom" customUserNamePasswordValidatorType="PM.Services.Security.UserValidator, PM.Services" />
<serviceCertificate findValue="PMSV" storeLocation="LocalMachine" storeName="TrustedPeople" x509FindType="FindBySubjectName" />
<clientCertificate>
<authentication certificateValidationMode="PeerTrust" />
</clientCertificate>
</serviceCredentials>
<serviceAuthorization principalPermissionMode="Custom">
<authorizationPolicies>
<add policyType='PM.Services.Security.PMAuthorizationPolicy, PM.Services' />
</authorizationPolicies>
</serviceAuthorization>
<ErrorHandler /> <!--Error handling behaviour-->
<serviceDebug includeExceptionDetailInFaults="false" />
</behavior>
...
<behaviors/>
<system.serviceModel/>
UPDATE Factory method i create the service with:
public static ServiceClient<TServiceContract> CreateService<TServiceContract>(object callbackInstance, string username, string password)
where TServiceContract: IServiceContract
{
Binding binding = STANDARD_BINDING;
bool supplyCredentials = true;
Type callbackImplementationType = null;
string protocol = "http";
string addr = "";
int port = PM_STANDARD_HTTP_PORT;
if (BINDING_MAPPING.ContainsKey(typeof(TServiceContract)))
{
var args = BINDING_MAPPING[typeof(TServiceContract)];
binding = (Binding)args[0];
protocol = (string)args[1];
object callbackType = args[2];
if (callbackType != null)
callbackImplementationType = (Type)callbackType;
supplyCredentials = (bool)args[3];
}
// Convention: Service .svc file in url is the same as contract
// without the leading "I"
// e.g. IFinanceService becomes FinanceService
// so url becomes
// http://localhost/PMServices/FinanceService.svc
string serviceName = typeof(TServiceContract).Name.Substring(1); // Remove leading I from interface name
string baseUri = Settings.GetString(iSetting.ServiceBaseURI);
UriBuilder b = new UriBuilder(baseUri);
if (protocol == "net.tcp")
port = PM_STANDARD_TCP_PORT;
addr = string.Format("{0}://{1}:{2}/{3}/{4}.svc",
protocol,
b.Host,
port,
b.Path.Replace("/", ""),
serviceName);
EndpointIdentity identity =
EndpointIdentity.CreateDnsIdentity("PMSV");
EndpointAddress endpointAddress =
new EndpointAddress(new Uri(addr), identity);
ChannelFactory<TServiceContract> channelFactory = null;
// Check whether service is duplex
if((binding.GetType() == typeof(NetTcpBinding) || binding.GetType() == typeof(WSDualHttpBinding))
&& (callbackImplementationType != null || callbackInstance != null))
{
object callbackImplementation = callbackInstance;
if(callbackImplementation == null && callbackImplementationType != null)
callbackImplementation = Activator.CreateInstance(callbackImplementationType, null);
if (callbackImplementation != null)
channelFactory = new DuplexChannelFactory<TServiceContract>(
callbackImplementation,
binding,
endpointAddress);
else // Create non-duplex channel if no callback implementation is specified
channelFactory = new ChannelFactory<TServiceContract>(binding,
endpointAddress);
}
else
channelFactory = new ChannelFactory<TServiceContract>(binding,
endpointAddress);
if (supplyCredentials)
{
channelFactory.Credentials.UserName.UserName =
username ?? PMClientPrincipal.Current.User.Name;
channelFactory.Credentials.UserName.Password =
password ?? ((PMClientIdentity)PMClientPrincipal
.Current
.User).Password;
}
return new ServiceClient<TServiceContract>(channelFactory,
channelFactory.CreateChannel());
}
PMClientPrincipal is my implementation of IPrincipal, and PMClientPrincipal.Current is a static property that just casts the current thread principal to PMClientPrincipal.
UPDATE The class that manages client subscriptions (used in the service)
public class DuplexServiceSubscriptionManager<TCallbackImplementation>
{
public DuplexServiceSubscriptionManager()
{
_subscribers = new List<TCallbackImplementation>();
}
private object _syncRoot = new object();
public List<TCallbackImplementation> _subscribers;
private void AddClient(TCallbackImplementation callback)
{
lock (_syncRoot)
{
if (!_subscribers.Contains(callback))
{
_subscribers.Add(callback);
((ICommunicationObject)callback).Closed += OnClientLost;
((ICommunicationObject)callback).Faulted += OnClientLost;
}
}
}
private void RemoveClient(TCallbackImplementation callback)
{
lock (_syncRoot)
{
if (_subscribers.Contains(callback))
{
_subscribers.Remove(callback);
((ICommunicationObject)callback).Closed -= OnClientLost;
((ICommunicationObject)callback).Faulted -= OnClientLost;
}
}
}
void OnClientLost(object sender, EventArgs e)
{
ServiceLogger.Log.DebugFormat("Client lost, reason: {0}", ((ICommunicationObject)sender).State);
if (OperationContext.Current == null)
return;
TCallbackImplementation callback = OperationContext.Current.GetCallbackChannel<TCallbackImplementation>();
RemoveClient(callback);
}
public bool Subscribe()
{
try
{
TCallbackImplementation callback = OperationContext.Current.GetCallbackChannel<TCallbackImplementation>();
AddClient(callback);
ServiceLogger.Log.Debug("Client subscribed.");
return true;
}
catch
{
return false;
}
}
public bool Unsubscribe()
{
try
{
TCallbackImplementation callback = OperationContext.Current.GetCallbackChannel<TCallbackImplementation>();
RemoveClient(callback);
ServiceLogger.Log.Debug("Client unsubscribed.");
return true;
}
catch
{
return false;
}
}
public void InvokeCallbackMethod(Action<TCallbackImplementation> target)
{
_subscribers.ForEach(delegate(TCallbackImplementation callback)
{
if (((ICommunicationObject)callback).State == CommunicationState.Opened)
{
try
{
target.Invoke(callback);
}
catch (Exception)
{
Unsubscribe();
}
}
else
{
Unsubscribe();
}
});
}
}
UPDATE Call stack to the callback method
System.ServiceModel.dll!System.ServiceModel.Dispatcher.SyncMethodInvoker.Invoke(object instance, object[] inputs, out object[] outputs) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage41(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage4(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage3(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage2(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage11(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage1(ref System.ServiceModel.Dispatcher.MessageRpc rpc) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.MessageRpc.Process(bool isOperationContextSet) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.Dispatch(ref System.ServiceModel.Dispatcher.MessageRpc rpc, bool isOperationContextSet) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ChannelHandler.DispatchAndReleasePump(System.ServiceModel.Channels.RequestContext request, bool cleanThread, System.ServiceModel.OperationContext currentOperationContext) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ChannelHandler.HandleRequest(System.ServiceModel.Channels.RequestContext request, System.ServiceModel.OperationContext currentOperationContext) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ChannelHandler.AsyncMessagePump(System.IAsyncResult result) Unknown
System.ServiceModel.dll!System.ServiceModel.Dispatcher.ChannelHandler.OnAsyncReceiveComplete(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously) Unknown
System.ServiceModel.Internals.dll!System.Runtime.InputQueue<System.ServiceModel.Channels.Message>.AsyncQueueReader.Set(System.Runtime.InputQueue<System.ServiceModel.Channels.Message>.Item item) Unknown
System.ServiceModel.Internals.dll!System.Runtime.InputQueue<System.ServiceModel.Channels.Message>.EnqueueAndDispatch(System.Runtime.InputQueue<System.ServiceModel.Channels.Message>.Item item, bool canDispatchOnThisThread) Unknown
System.ServiceModel.Internals.dll!System.Runtime.InputQueue<System.ServiceModel.Channels.Message>.EnqueueAndDispatch(System.ServiceModel.Channels.Message item, System.Action dequeuedCallback, bool canDispatchOnThisThread) Unknown
System.ServiceModel.Internals.dll!System.Runtime.InputQueue<System.__Canon>.EnqueueAndDispatch(System.__Canon item) Unknown
System.ServiceModel.dll!System.ServiceModel.Security.SecuritySessionClientSettings<System.ServiceModel.Channels.IDuplexSessionChannel>.ClientSecurityDuplexSessionChannel.CompleteReceive(System.IAsyncResult result) Unknown
System.ServiceModel.dll!System.ServiceModel.Security.SecuritySessionClientSettings<System.ServiceModel.Channels.IDuplexSessionChannel>.ClientSecurityDuplexSessionChannel.OnReceive(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously, System.Exception exception) Unknown
System.ServiceModel.dll!System.ServiceModel.Security.SecuritySessionClientSettings<System.ServiceModel.Channels.IDuplexSessionChannel>.ClientSecuritySessionChannel.ReceiveAsyncResult.OnReceive(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously, System.Exception exception) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.ReliableChannelBinder<System.ServiceModel.Channels.IDuplexSessionChannel>.InputAsyncResult<System.ServiceModel.Channels.ReliableChannelBinder<System.ServiceModel.Channels.IDuplexSessionChannel>>.OnInputComplete(System.IAsyncResult result) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.ReliableChannelBinder<System.ServiceModel.Channels.IDuplexSessionChannel>.InputAsyncResult<System.ServiceModel.Channels.ReliableChannelBinder<System.ServiceModel.Channels.IDuplexSessionChannel>>.OnInputCompleteStatic(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously, System.Exception exception) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.TransportDuplexSessionChannel.TryReceiveAsyncResult.OnReceive(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.Fx.AsyncThunk.UnhandledExceptionFrame(System.IAsyncResult result) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously) Unknown
System.ServiceModel.Internals.dll!System.Runtime.AsyncResult.Complete(bool completedSynchronously, System.Exception exception) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SynchronizedMessageSource.SynchronizedAsyncResult<System.ServiceModel.Channels.Message>.CompleteWithUnlock(bool synchronous, System.Exception exception) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SynchronizedMessageSource.ReceiveAsyncResult.OnReceiveComplete(object state) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SessionConnectionReader.OnAsyncReadComplete(object state) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SocketConnection.FinishRead() Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SocketConnection.OnReceiveAsync(object sender, System.Net.Sockets.SocketAsyncEventArgs eventArgs) Unknown
System.ServiceModel.dll!System.ServiceModel.Channels.SocketConnection.OnReceiveAsyncCompleted(object sender, System.Net.Sockets.SocketAsyncEventArgs e) Unknown
System.dll!System.Net.Sockets.SocketAsyncEventArgs.OnCompleted(System.Net.Sockets.SocketAsyncEventArgs e) Unknown
System.dll!System.Net.Sockets.SocketAsyncEventArgs.FinishOperationSuccess(System.Net.Sockets.SocketError socketError, int bytesTransferred, System.Net.Sockets.SocketFlags flags) Unknown
System.dll!System.Net.Sockets.SocketAsyncEventArgs.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) Unknown
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) Unknown