Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I safely set the user principal in a custom WebAPI HttpMessageHandler?

For basic authentication I have implemented a custom HttpMessageHandler based on the example shown in Darin Dimitrov's answer here: https://stackoverflow.com/a/11536349/270591

The code creates an instance principal of type GenericPrincipal with user name and roles and then sets this principal to the current principal of the thread:

Thread.CurrentPrincipal = principal; 

Later in a ApiController method the principal can be read by accessing the controllers User property:

public class ValuesController : ApiController {     public void Post(TestModel model)     {         var user = User; // this should be the principal set in the handler         //...     } } 

This seemed to work fine until I recently added a custom MediaTypeFormatter that uses the Task library like so:

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream,     HttpContent content, IFormatterLogger formatterLogger) {     var task = Task.Factory.StartNew(() =>     {         // some formatting happens and finally a TestModel is returned,         // simulated here by just an empty model         return (object)new TestModel();     });     return task; } 

(I have this approach to start a task with Task.Factory.StartNew in ReadFromStreamAsync from some sample code. Is it wrong and maybe the only reason for the problem?)

Now, "sometimes" - and for me it appears to be random - the User principal in the controller method isn't the principal anymore I've set in the MessageHandler, i.e. user name, Authenticated flag and roles are all lost. The reason seems to be that the custom MediaTypeFormatter causes a change of the thread between MessageHandler and controller method. I've confirmed this by comparing the values of Thread.CurrentThread.ManagedThreadId in the MessageHandler and in the controller method. "Sometimes" they are different and then the principal is "lost".

I've looked now for an alternative to setting Thread.CurrentPrincipal to somehow transfer the principal safely from the custom MessageHandler to the controller method and in this blog post request properties are used:

request.Properties.Add(HttpPropertyKeys.UserPrincipalKey,     new GenericPrincipal(identity, new string[0])); 

I wanted to test that but it seems that the HttpPropertyKeys class (which is in namespace System.Web.Http.Hosting) doesn't have a UserPrincipalKey property anymore in the recent WebApi versions (release candidate and final release from last week as well).

My question is: How can I change the last code snippet above so that is works with the current WebAPI version? Or generally: How can I set the user principal in a custom MessageHandler and access it reliably in a controller method?

Edit

It is mentioned here that "HttpPropertyKeys.UserPrincipalKey ... resolves to “MS_UserPrincipal”", so I tried to use:

request.Properties.Add("MS_UserPrincipal",     new GenericPrincipal(identity, new string[0])); 

But it doesn't work as I expected: The ApiController.User property does not contain the principal added to the Properties collection above.

like image 914
Slauma Avatar asked Aug 19 '12 17:08

Slauma


People also ask

How can you set the principal?

Setting the Principal If your application performs any custom authentication logic, you must set the principal on two places: Thread. CurrentPrincipal. This property is the standard way to set the thread's principal in .

How do I provide authentication in Web API?

To access the web API method, we have to pass the user credentials in the request header. If we do not pass the user credentials in the request header, then the server returns 401 (unauthorized) status code indicating the server supports Basic Authentication.

How does authentication and authorization work in Web API?

The authentication and authorization mechanism in such a site is simple. After the user logs into the website, a single database holding user information verifies their identity. A session is created on the server, and all subsequent requests use the session to identify the user without another login required.


2 Answers

The problem of losing the principal on a new thread is mentioned here:

http://leastprivilege.com/2012/06/25/important-setting-the-client-principal-in-asp-net-web-api/

Important: Setting the Client Principal in ASP.NET Web API

Due to some unfortunate mechanisms buried deep in ASP.NET, setting Thread.CurrentPrincipal in Web API web hosting is not enough.

When hosting in ASP.NET, Thread.CurrentPrincipal might get overridden with HttpContext.Current.User when creating new threads. This means you have to set the principal on both the thread and the HTTP context.

And here: http://aspnetwebstack.codeplex.com/workitem/264

Today, you will need to set both of the following for user principal if you use a custom message handler to perform authentication in the web hosted scenario.

IPrincipal principal = new GenericPrincipal(     new GenericIdentity("myuser"), new string[] { "myrole" }); Thread.CurrentPrincipal = principal; HttpContext.Current.User = principal; 

I have added the last line HttpContext.Current.User = principal (needs using System.Web;) to the message handler and the User property in the ApiController does always have the correct principal now, even if the thread has changed due to the task in the MediaTypeFormatter.

Edit

Just to emphasize it: Setting the current user's principal of the HttpContext is only necessary when the WebApi is hosted in ASP.NET/IIS. For self-hosting it is not necessary (and not possible because HttpContext is an ASP.NET construct and doesn't exist when self hosted).

like image 137
Slauma Avatar answered Oct 14 '22 12:10

Slauma


To avoid the context switch try using a TaskCompletionSource<object> instead of manually starting another task in your custom MediaTypeFormatter:

public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger) {     var tcs = new TaskCompletionSource<object>();      // some formatting happens and finally a TestModel is returned,     // simulated here by just an empty model     var testModel = new TestModel();      tcs.SetResult(testModel);     return tcs.Task; } 
like image 33
Darin Dimitrov Avatar answered Oct 14 '22 11:10

Darin Dimitrov