Search code examples
wcfcachingasynchronoustokenwif

Wif secured WCF service, caching tokens - async wcf method loses identity


I have a wcf service that is secured using WIF. I'm implementing token caching on the client (a website), as described in Travis Spencer's blog here:

http://travisspencer.com/blog/2009/03/caching-tokens-to-avoid-calls.html

The website is using impersonation (impersonating me), and the STS wcf endpoint is configured to use Windows authentication.

When calling the WCF service using direct calls (i.e. non async calls), token caching works fine - the ClientCredentials behavior is removed and my custom CacheClientCredentials behavior is added, and the token is cached on the first call and reused on the subsequent call.

However I have a scenario in which an async method is called on the wcf service, with a callback being provided. Under normal circumstances (non async) the CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider method is called multiple times, and on each call the identity the thread is running as is correct. The subsequent call to the STS is using the correct credentials, the user is authenticated and the token returned. When the async method is called, the multiple calls to CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider occur, but only the first call has the correct identity. The subsequent calls have "NT AUTHORITY\NETWORK SERVICE" as the identity. As a result, the call to the STS has the wrong credentials, and the authentication fails. (The STS log shows a message indicating " could not be authenticated".

I have been trying adding explicit impersonation around the Begin/End async methods, but this didn't work all the time. I then added the following values to the web.config:

    <legacyImpersonationPolicy enabled="false" />
    <alwaysFlowImpersonationPolicy enabled="true" />

This also doesn't always work (although sometimes does). The unusual thing here is that the business logic involves 3 attempts to call the async method (if calls fail). I am finding that generally the first 2 fail, and the third succeeds - i.e. CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider normally runs as the wrong identity the first two times, and the correct identity the third time - but this appears to be a bit random too. When CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider succeeds with the right identity, the WCF call succeeds. However the token is not added to the cache (no token cache code is executed) Subsequent calls to a non async method then construct a new cache and then get the token, adding it to the cache.

What is the correct method to call an asynchronous WCF method that is secured using WIF, to ensure that the WIF token is cached across calls?

Is there special identity configuration required to ensure the same identity is used across all of these processes? (all areas are using impersonation)

Update:

I'm not sure if it adds much, but I've found that when the code doesn't work, the following is the stacktrace while in the CacheClientCredentialsSecurityTokenManager:

Unflagged   >   5732    18  Worker Thread   <No Name>   CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider  Normal
                        MySecurity.dll!CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider(System.IdentityModel.Selectors.SecurityTokenRequirement tokenRequirement) Line 18  
                        System.ServiceModel.dll!System.ServiceModel.Security.SecurityProtocol.AddSupportingTokenProviders(System.ServiceModel.Security.Tokens.SupportingTokenParameters supportingTokenParameters, bool isOptional, System.Collections.Generic.IList<System.ServiceModel.Security.SupportingTokenProviderSpecification> providerSpecList) + 0xca bytes   
                        System.ServiceModel.dll!System.ServiceModel.Security.SecurityProtocol.OnOpen(System.TimeSpan timeout) + 0xa7 bytes   
                        System.ServiceModel.dll!System.ServiceModel.Security.SymmetricSecurityProtocol.OnOpen(System.TimeSpan timeout) + 0x45 bytes  
                        System.ServiceModel.dll!System.ServiceModel.Security.OperationWithTimeoutAsyncResult.OnScheduled(object state) + 0x82 bytes  
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke2() + 0x46 bytes    
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.OnSecurityContextCallback(object o) + 0x28 bytes  
                        mscorlib.dll!System.Security.SecurityContext.Run(System.Security.SecurityContext securityContext, System.Threading.ContextCallback callback, object state) + 0x55 bytes  
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke() + 0x4d bytes     
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ProcessCallbacks() + 0x180 bytes   
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.CompletionCallback(object state) + 0x7a bytes  
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ScheduledOverlapped.IOCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) + 0xf bytes     
                        SMDiagnostics.dll!System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(uint error, uint bytesRead, System.Threading.NativeOverlapped* nativeOverlapped) + 0x3d bytes    
                        mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) + 0x54 bytes     
                        [Appdomain Transition]  

When it does work, it is a bit different (note the additional mscorlib entries, and transitions in the middle):

Unflagged   >   5408    12  Worker Thread   <No Name>   CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider  Normal
                        MySecurity.dll!CacheClientCredentialsSecurityTokenManager.CreateSecurityTokenProvider(System.IdentityModel.Selectors.SecurityTokenRequirement tokenRequirement) Line 18  
                        System.ServiceModel.dll!System.ServiceModel.Security.SecurityProtocol.AddSupportingTokenProviders(System.ServiceModel.Security.Tokens.SupportingTokenParameters supportingTokenParameters, bool isOptional, System.Collections.Generic.IList<System.ServiceModel.Security.SupportingTokenProviderSpecification> providerSpecList) + 0xca bytes   
                        System.ServiceModel.dll!System.ServiceModel.Security.SecurityProtocol.OnOpen(System.TimeSpan timeout) + 0xa7 bytes   
                        System.ServiceModel.dll!System.ServiceModel.Security.SymmetricSecurityProtocol.OnOpen(System.TimeSpan timeout) + 0x45 bytes  
                        System.ServiceModel.dll!System.ServiceModel.Security.OperationWithTimeoutAsyncResult.OnScheduled(object state) + 0x82 bytes  
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke2() + 0x46 bytes    
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.OnSecurityContextCallback(object o) + 0x28 bytes  
                        ***mscorlib.dll!System.Security.SecurityContext.runTryCode(object userData) + 0x6e bytes     
                        [Native to Managed Transition]   
                        [Managed to Native Transition]   
                        ***mscorlib.dll!System.Security.SecurityContext.RunInternal(System.Security.SecurityContext securityContext, System.Threading.ContextCallback callBack, object state) + 0xc2 bytes   
                        ***mscorlib.dll!System.Security.SecurityContext.Run(System.Security.SecurityContext securityContext, System.Threading.ContextCallback callback, object state) + 0xca bytes   
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.WorkItem.Invoke() + 0x4d bytes     
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ProcessCallbacks() + 0x180 bytes   
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.CompletionCallback(object state) + 0x7a bytes  
                        System.ServiceModel.dll!System.ServiceModel.Channels.IOThreadScheduler.CriticalHelper.ScheduledOverlapped.IOCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped) + 0xf bytes     
                        SMDiagnostics.dll!System.ServiceModel.Diagnostics.Utility.IOCompletionThunk.UnhandledExceptionFrame(uint error, uint bytesRead, System.Threading.NativeOverlapped* nativeOverlapped) + 0x3d bytes    
                        mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) + 0x54 bytes     
                        [Appdomain Transition]   

Solution

  • I have not been explicitly calling .Open() on the proxy before attempting to call the async method, and as a result the .Open() appears to happen internally within the client proxy, on another thread - hence the identity issues. I found that if I called:

    If _proxy.State <> CommunicationState.Opened Then
         _proxy.Open()
    End If
    
    _proxy.Begin[asyncMethod]()
    

    the cached credentials setup and token cache checking happens synchronously, and therefore using the correct identity, and functions as expected.