views:

547

answers:

3

I was doing some unit testing, and ran into an interesting problem in WCF. I have a service using the wsHttpBinding, configured as such:

<bindings>
  <wsHttpBinding>
    <binding name="wsHttpUnsecured">
      <security mode="None">
        <transport clientCredentialType="None" />
        <message clientCredentialType="None" />
      </security>
    </binding>
  </wsHttpBinding>

The implementation of the service is:

public void DoWork()
{
    OperationContext o = OperationContext.Current;
    if (o == null) return;

    if (o.ServiceSecurityContext.AuthorizationContext.Properties.ContainsKey("x"))
        throw new ApplicationException();

    o.ServiceSecurityContext.AuthorizationContext.Properties.Add("x", "x");
}

All it is doing here is checking the operation context, and if there is not an "X" in the AuthorizationContext, then add one. If there was already an "X", then exception out. (this is set up strictly as a simple test. in normal use, this would be happening in a custom AuthorizationManager and Token Authenticator).

So basically, if we call the method more than once within the same operationContext and AuthorizationContext, then we will see an exception.

Now, here is my test fixture.

 [Test]
 public void CallTwice()
 {
  using (var cli1 = new TestBusinessClient())
  {
   cli1.DoWork();
   cli1.Close();
  }
  using (var cli2 = new TestBusinessClient())
  {
   cli2.DoWork();
   cli2.Close();
  }
 }

So walking through what happens at runtime:

  1. A new TestBusinessClient is created.
  2. It makes a call to DoWork().
  3. DoWork() does not find "X" in the AuthorizationContext.Properties.
  4. DoWork() adds "X" to the AuthorizationContext.Properties.
  5. The test method disposes of the first client.
  6. A new second TestBusinessClient is created.
  7. It makes a call to DoWork().
  8. DoWork() does find "X" still in the properties from the last call.
  9. DoWork() throws an exception.

So my question is; why is the OperationContext and AuthorizationContext not killed off when a new client connects to the same service? I do understand that wsHttpBinding by default uses a session context between the calls, but I would think that session would be per client. I expected the wcf session,a nd therefore its contexts, to all renew when I connect with a new instance of a client.

Anyone have any thoughts or ideas? The desired result here is for AuthorizationContext.Properties to be reset between the two calls to DoWork() by the two separate clients.


Update 1

I tried setting the service PerCall and PerSession, and neither made a difference.

I also tried the service with reliable messaging on and off, and neither changed anything.

I also saved off my operationContext at the first call and the second call, and compared the two:

OperationContext first; // context from first call to DoWork()
OperationContext second; // context from second call to DoWork() 

(first == second) = false
(first.ServiceSecurityContext == second.ServiceSecurityContext) = false
(first.ServiceSecurityContext.AuthorizationContext == second.ServiceSecurityContext.AuthorizationContext) = true

So it kind of looks like the operation context is changed / recreated, but something is setting the same AuthorizationContext on each subsequent service call.


Update 2

Here is all the server-side stuff:

[ServiceContract]
public interface ITestBusiness
{
 [OperationContract(Action = "*", ReplyAction = "*")]
    string DoWork();
}

public class TestBusiness : ITestBusiness
{
    public string DoWork()
    {
  System.ServiceModel.OperationContext o = System.ServiceModel.OperationContext.Current;
  if (o != null)
  {
   if (o.ServiceSecurityContext.AuthorizationContext.Properties.ContainsKey("x"))
    throw new ApplicationException();
   else
    o.ServiceSecurityContext.AuthorizationContext.Properties.Add("x", "x");
  }
    }
    return "";
}

As a sanity check, I did the following:

  1. Start an instance of the wcf server (using Cassini / integrated VS 2008 server).
  2. Reduce teh test fixture to only 1 call to DoWork().
  3. Run the test from TestDriven.NET winthin VS 2008.
  4. Open the same test .dll from NUnit's standalone GUI tool, and run it.

The first test run passes, and the second fails. So it seems this is purely server side, as running the same service call from 2 different processes ends up with the same AuthorizationContext.

I'm starting to wonder if maybe something internal to WCF is still stuck on WindowsAuthentication and reusing the same Auth Context since I'm logged into the same domain with the same user name? My service is set to use a custom AuthorizationManager:

  <serviceBehaviors>
   <behavior name="myBehavior">
    <serviceMetadata httpGetEnabled="false"  />
    <serviceDebug includeExceptionDetailInFaults="true" />
    <serviceAuthorization principalPermissionMode="Custom" serviceAuthorizationManagerType="My.Namespace.CustomAuthMgr" />
   </behavior>

Where My.Namespace.CustomAuthMgr extends ServiceAuthorizationManager. If I set a breakpoint in CustomAuthMgr.CheckAccess(), then on the first call, operationContext.ServiceSecurityContext.AuthorizationContext is clear, and ont he second call, it contains whatever I put in it from the previous call. This is the 1st method of my own code that is executed by WCF, so something before the Authorization phase is reloading my AuthorizationContext.


Update 3

Some added info: In order to validate some things, I changes my service implementation to no longer throw an exception, and instead return a counter of the # of times called, plus the current thread ID:

    public string DoWork()
    {
  var o = System.ServiceModel.OperationContext.Current.ServiceSecurityContext.AuthorizationContext;
  if (o != null)
  {
   if (o.Properties.ContainsKey("x"))
    o.Properties["x"] = (int)o.Properties["x"] + 1;
   else
    o.Properties.Add("x", 1);
  }

  return o.Properties["x"].ToString() + " | " + System.AppDomain.GetCurrentThreadId().ToString();
    }

Then running the test once from NUnit GUI results in: 1 | 3816

I then close the NUnit GUI, restart it, and run the test again: 2 | 3816

I then close the NUnit GUI again,a nd run the test from TestDriven.NET within Visual Studio: 3 | 3816

So its definately persisting my AuthorizationContext between client processes, but the same thread handles each service call, so maybe AuthorizationContext is just Thread-static or something?

Nope, it has nothing to do with the thread. I added a Thread.Sleep(10000); to the service implementation, then ran 2 NUnit GUIs at once, and each one printed out "2" but with different thread IDs:

2 | 5500
2 | 5764

So AuthorizationContext is being persisted across threads too. Nifty.

+1  A: 

That really depends on how you are defining session and client. If you are making two calls from the same process, there is every possibility that the WCF communication layer is multiplexing them down the same connection.

Have you actually tried to run two connects from entirely different processes? Do you achieve the same results?

Christopher
I tried starting the service (in Cassini / VS 2008 integrated web server), then running the unit test once, which passes, and closes the nunit session. Then ran the unit test a 2nd time, and it fails. I ran these 2 back to back with TestDriven.NET within VS 2008. I then opened NUnit GUI tool (external to VS2008) loaded the same test .dll, and the test still failed, all running against the same server. So 3 test runs, from 2 different process chains, and only the 1st one to run passes.
rally25rs
I am also surprised by your results. A good way to track this down might be to turn on security auditing in WCF. It's just a behavior your can attach to your service. The feault is AuditLevel.None, but if you turn it way up it might give you some more transparency about what is happening and when.
Christopher
+1  A: 

It depends on the configuration of your service, what value you choose for your SessionMode:

[ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples", SessionMode=SessionMode.Required)]
public interface ICalculatorSession

http://msdn.microsoft.com/en-us/library/ms733040.aspx

http://msdn.microsoft.com/en-us/library/system.servicemodel.sessionmode.aspx

Shiraz Bhaiji
I tried both SessionMode.NotAllowed and SessionMode.Required, with no difference.
rally25rs
You need also to set InstanceContextMode this can be InstanceContextMode.PerSession / PerCall / single, the second link has some info on this.
Shiraz Bhaiji
I tried both PerSession and PerCall with no change in the result. As noted in my Update #2 above, I also started calling the service once from 2 different processes, and still the same result.
rally25rs
+1  A: 

WsHttp binding will by default run in session mode, since security is turned on by default. If you turn it off completely (as your config seems to show), it'll revert to per-call mode, unless your service contract specifically requires a session.

The session will last as long as none of the session timeouts have occured - you can define one on the server, and another one on the client, and the shorter one "wins". The easiest way to specify it is by adding the reliable messaging to the wsHttpBinding:

  <wsHttpBinding>
    <binding>
      <reliableSession inactivityTimeout="00:10:00"/>
    </binding>
  </wsHttpBinding>

Otherwise you'll have to do it in code or by defining your custom binding.

OR: you can also define certain methods on your service contract to be "terminating" methods, and if those have been called and terminate, the session will be destroyed as well.

[ServiceContract(SessionMode=SessionMode.Required)]
interface YourServiceContract
{
    [OperationContract(IsInitiating = true)]
    void InitMethod();

    [OperationContract]
    void WorkMethod()

    [OperationContract(IsTerminating=true)]
    void EndSessionMethod()
}

Here, calling the EndSessionMethod will terminate the session and no more calls can be made to the service on the particular proxy instance.

OR: an unhandled exception on the server side which is not caught and turned into a FaultException will also fault the channel and abort any session that might have been ongoing.

But as you point out - I, too, would have expected the entire session and all associated data to be discarded when the client proxy is disposed of, and recreated for a subsequent call. I'm a bit surprised by your findings....

Marc

marc_s
So, @marc_s: incorrectly using a "using" block as the OP did will not automatically end the session?
John Saunders
Well, that's what I would have expected, and the OP, too, but it seems it's not really disposing of the OperationContext and/or the AuthorizationContext on the server
marc_s
But I agree - while the `using` statement is normally a good idea, in WCF land, it can be a bit deceiving to think it works as usual - but that's advanced stuff already :-)
marc_s
sorry if the "using" block is confusing. In the "real world" (outside the simplicity of this test) my test clients all extend a custom base class that is IDisposable, and in that base class's Dispose, it makes sure that .Close() is called on the client, just incase other developers (other team members) forget to. It was basically my way of ensuring that .Close is always called on the clients. (Its also called in the GC finalizer, so the using block isnt even totally needed).
rally25rs
Also note in my Update #2 above, I started calling the service from 2 separate processes on the same machine, and the same result on the server.
rally25rs
Yes, the point with WCF client proxies is: since there is a chance that due to an error, the proxy will be faulted, it can throw an exception when the using() is trying to Dispose() it. While the using() is a good idea most of the times, here in WCF proxy land, it can lead to unexpected and unwanted side effects.
marc_s
Therefore, it's considered best practice to handle the proxy in your own explicit try...catch...finally block and check for conditions that would prevent you from properly closing the proxy, and in that case, call the .Abort() method on the proxy instead of the .Close() - you can't do that when you have the proxy use in a using() statement.
marc_s