From what I've read online, context-sensitive items such as security (Thread Principal), culture, etc, should flow across asynchronous threads within the bounds of an execution unit of work.
I'm encountering very confusing and potentially dangerous bugs though. I'm noticing my thread's CurrentPrincipal is getting lost across async execution.
Here is an example ASP.NET Web API scenario:
First, let's setup a simple Web API configuration with two delegating handlers for testing purposes.
All they do is write out debug information and pass the request/response on through, except the first "DummyHandler" which sets the thread's principal as well as a piece of data to be shared across the context (the request's correlation ID).
public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MessageHandlers.Add(new DummyHandler()); config.MessageHandlers.Add(new AnotherDummyHandler()); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } public class DummyHandler : DelegatingHandler { protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { CallContext.LogicalSetData("rcid", request.GetCorrelationId()); Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest"))); Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid")); return base.SendAsync(request, cancellationToken) .ContinueWith(task => { Debug.WriteLine("Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine("User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine("RCID: {0}", CallContext.LogicalGetData("rcid")); return task.Result; }); } } public class AnotherDummyHandler : MessageProcessingHandler { protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) { Debug.WriteLine(" Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return request; } protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) { Debug.WriteLine(" Another Dummy Handler Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return response; } }
Simple enough. Next let's add a single ApiController to handle an HTTP POST, as if you were uploading files.
public class UploadController : ApiController { public async Task<HttpResponseMessage> PostFile() { Debug.WriteLine(" Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); if (!Request.Content.IsMimeMultipartContent()) { throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); } try { await Request.Content.ReadAsMultipartAsync( new MultipartFormDataStreamProvider( HttpRuntime.AppDomainAppPath + @"upload\temp")); Debug.WriteLine(" Thread: {0}", Thread.CurrentThread.ManagedThreadId); Debug.WriteLine(" User: {0}", (Object)Thread.CurrentPrincipal.Identity.Name); Debug.WriteLine(" RCID: {0}", CallContext.LogicalGetData("rcid")); return new HttpResponseMessage(HttpStatusCode.Created); } catch (Exception e) { return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e); } } }
Upon running a test with Fiddler, this is the output I receive:
Dummy Handler Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Another Dummy Handler Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Thread: 63 User: dgdev RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Thread: 77 User: <<< PRINCIPAL IS LOST AFTER ASYNC RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Another Dummy Handler Thread: 63 User: <<< PRINCIPAL IS STILL LOST RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476 Dummy Handler Thread: 65 User: dgdev <<< PRINCIPAL IS BACK?!? RCID: 6d542847-4ceb-4511-85e5-d1b5bf3be476
To make matters more confusing, When I append the follow to the async line:
await Request.Content.ReadAsMultipartAsync( new MultipartFormDataStreamProvider(..same as before..)) .ConfigureAwait(false); <<<<<<
I now receive this output:
Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Another Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Thread: 65 User: dgdev <<< PRINCIPAL IS HERE! RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Another Dummy Handler Thread: 65 User: <<< PRINCIPAL IS LOST RCID: 8d944500-cb52-4362-8537-dab405fa12a2 Dummy Handler Thread: 40 User: dgdev RCID: 8d944500-cb52-4362-8537-dab405fa12a2
The point here is this. The code following the async my in fact call my business logic or simply require the security context be properly set. There is a potential integrity problem going on.
Can anyone help shed some light one what is happening?
Thanks in advance.
I don't have all the answers, but I can help fill in some blanks and guess at the problem.
By default, the ASP.NET SynchronizationContext
will flow, but the way it flows identity is a bit weird. It actually flows HttpContext.Current.User
and then sets Thread.CurrentPrincipal
to that. So if you just set Thread.CurrentPrincipal
, you won't see it flow correctly.
In fact, you'll see the following behavior:
Thread.CurrentPrincipal
is set on a thread, that thread will have that same principal until it re-enters an ASP.NET context.Thread.CurrentPrincipal
is cleared (because it's set to HttpContext.Current.User
).Thread.CurrentPrincipal
happened to be set on it.Applying this to your original code and output:
CurrentPrincipal
was explicitly set, so they all have the expected value.async
method, thus entering the ASP.NET context and clearing any CurrentPrincipal
it may have had.ProcessResponse
. It re-enters the ASP.NET context, clearing its Thread.CurrentPrincipal
.ContinueWith
without a scheduler), so it just retains whatever CurrentPrincipal
it happened to have before. I assume that its CurrentPrincipal
is just left over from an earlier test run.The updated code changes PostFile
to run its second portion outside the ASP.NET context. So it picks up thread 65, which just happens to have CurrentPrincipal
set. Since it's outside the ASP.NET context, CurrentPrincipal
isn't cleared.
So, it looks to me like ExecutionContext
is flowing fine. I'm sure Microsoft has tested ExecutionContext
flow out the wazoo; otherwise every ASP.NET app in the world would have a serious security flaw. It's important to note that in this code Thread.CurrentPrincipal
just refers to the current user's claims and does not represent actual impersonation.
If my guesses are correct, then the fix is quite simple: in SendAsync
, change this line:
Thread.CurrentPrincipal = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest")));
to this:
HttpContext.Current.User = new ClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new[]{ new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "dgdev") }, "myauthisthebest"))); Thread.CurrentPrincipal = HttpContext.Current.User;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With