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]
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.