I set the current user to a ClaimsIdentity in a WebAPI Messagehandler derived from DelegateHandler, and retrieve the username and user id from the controller.
It used to work quite fine. Now something has changed (In RC, and probably the last Preview version too) and instead of getting my ClaimsIdentity in the ApiController I suddenly get a WindowsIdentity (unauthenticated, though). I thought maybe some window authentication was kicking in, but I've never used that, and it is disabled under the properties window for the project (also the WIndowsIdentify I receive is not actually authenticated).
It's practically impossible to find examples with the new RC. Did something break, or am I doing something wrong? While I'm at it, some info on the new AuthenticationFilters would be awesome, thanks!
I can set breakpoints and trace the Identity being set correctly to Thread.Principal AND to HttpContext.Current.User in the messagehandler, and suddenly it's gone in the controller. I can send a demo project if required, but since its more than 4mb I can't attach it.
Key elements:
public class AuthHandler : DelegatingHandler
{
protected async override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
string apiKey = request.GetQueryNameValuePairs().FirstOrDefault(k => k.Key == "apiKey").Value;
if (!string.IsNullOrWhiteSpace(apiKey))
{
string[] token = apiKey.Split('*');
var claims = new List<Claim>{
new Claim(ClaimTypes.Name, token[1]),
new Claim(ClaimTypes.NameIdentifier, token[0])
};
var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
HttpContext.Current.User = principal;
}
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response = request.CreateResponse(HttpStatusCode.Unauthorized);
}
return response;
}
}
}
Test Controller:
public IEnumerable<string> Get()
{
var id = (System.Security.Claims.ClaimsIdentity)User.Identity;
if (id.Claims.FirstOrDefault(i => i.Type == System.Security.Claims.ClaimTypes.NameIdentifier) != null)
{
var userId = id.Claims.FirstOrDefault(i => i.Type == System.Security.Claims.ClaimTypes.NameIdentifier).Value;
return new string[] { id.Name, userId };
}
return new string[] { "No", "User", "Returned" };
}
Test call:
http://localhost:1646/api/test?apiKey=12345667*myusername
Comments: mrlund, Sure. The existing code above worked in Web API 1 and should continue to work in Web API 2 with the last night's build. For new code, there are two changes I would suggest (both optional): First: I'd recommend setting HttpRequestContext.Principal instead of HttpContext.Current.User and Thread.CurrentPrincipal. The current code is fine for web host and the legacy self host. But for OWIN host, it won't always be right. If HttpRequestContext.Principal is a new abstraction that hides the host-specific details for you; it will make sure the principal is set the right way regardless of host. So instead of: Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) HttpContext.Current.User = principal; Do: request.GetRequestContext().Principal = principal; Second: For new code, I'd recommend using authentication filters rather than message handlers for doing authentication. Authentication filters are a higher-level abstraction and that take care of some of the details for you, like setting the principal on the host and multiple authentication mechanisms cooperate together. To plug in an authentication filter, you can do one of three things: a. Add the filter globally (for all controllers/actions), like: config.Filters.Add(new ApiKeyAuthenticationFilter()); b. Add the filter to one specific controller (all of its actions) via an attribute, like: [ApiKeyAuthenticationFilter] public class TestController : ApiController { /* ... */ } c. Add the filter to one specific action via an attribute, like: public class TestController : ApiController { [ApiKeyAuthenticationFilter] public IEnumerable<string> Get() { /* ... */ } } Note that you'll need to have your filter implementation inherit from Attribute if you want to use options b or c. Below is a sample authentication filter for the ApiKey scheme above (for global use only; it doesn't inherit from Attribute). public class ApiKeyAuthenticationFilter : IAuthenticationFilter { public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { HttpRequestMessage request = context.Request; string apiKey = request.GetQueryNameValuePairs().FirstOrDefault(k => k.Key == "apiKey").Value; if (string.IsNullOrWhiteSpace(apiKey)) { // Return indicating no authentication was attempted (for this authentication mechanism). return Task.FromResult(0); } string[] token = apiKey.Split('*'); if (token.Length != 2) { // Return indicating authentication was attempted but failed. context.ErrorResult = new BadRequestTextPlainResult { Content = "The apiKey query string parameter must be in the format x*y.", Request = request }; return Task.FromResult(0); } var claims = new List<Claim>{ new Claim(ClaimTypes.Name, token[1]), new Claim(ClaimTypes.NameIdentifier, token[0]) }; var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") }); // Return indicating authentication was attempted and succeeded. context.Principal = principal; return Task.FromResult(0); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { // Nothing to do for this authentication mechanism since it is query string based. A mechanism that uses // the WWW-Authenticate header would update the result to add its WWW-Authenticate challenge; // something like context.Result = new AddChallengeOn401Result(context.Result); return Task.FromResult(0); } public bool AllowMultiple { get { return false; } } private class BadRequestTextPlainResult : IHttpActionResult { public string Content { get; set; } public HttpRequestMessage Request { get; set; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest); response.Content = new StringContent(Content); response.RequestMessage = Request; return Task.FromResult(response); } } }
It used to work quite fine. Now something has changed (In RC, and probably the last Preview version too) and instead of getting my ClaimsIdentity in the ApiController I suddenly get a WindowsIdentity (unauthenticated, though). I thought maybe some window authentication was kicking in, but I've never used that, and it is disabled under the properties window for the project (also the WIndowsIdentify I receive is not actually authenticated).
It's practically impossible to find examples with the new RC. Did something break, or am I doing something wrong? While I'm at it, some info on the new AuthenticationFilters would be awesome, thanks!
I can set breakpoints and trace the Identity being set correctly to Thread.Principal AND to HttpContext.Current.User in the messagehandler, and suddenly it's gone in the controller. I can send a demo project if required, but since its more than 4mb I can't attach it.
Key elements:
public class AuthHandler : DelegatingHandler
{
protected async override System.Threading.Tasks.Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)
{
string apiKey = request.GetQueryNameValuePairs().FirstOrDefault(k => k.Key == "apiKey").Value;
if (!string.IsNullOrWhiteSpace(apiKey))
{
string[] token = apiKey.Split('*');
var claims = new List<Claim>{
new Claim(ClaimTypes.Name, token[1]),
new Claim(ClaimTypes.NameIdentifier, token[0])
};
var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null)
HttpContext.Current.User = principal;
}
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
response = request.CreateResponse(HttpStatusCode.Unauthorized);
}
return response;
}
}
}
Test Controller:
public IEnumerable<string> Get()
{
var id = (System.Security.Claims.ClaimsIdentity)User.Identity;
if (id.Claims.FirstOrDefault(i => i.Type == System.Security.Claims.ClaimTypes.NameIdentifier) != null)
{
var userId = id.Claims.FirstOrDefault(i => i.Type == System.Security.Claims.ClaimTypes.NameIdentifier).Value;
return new string[] { id.Name, userId };
}
return new string[] { "No", "User", "Returned" };
}
Test call:
http://localhost:1646/api/test?apiKey=12345667*myusername
Comments: mrlund, Sure. The existing code above worked in Web API 1 and should continue to work in Web API 2 with the last night's build. For new code, there are two changes I would suggest (both optional): First: I'd recommend setting HttpRequestContext.Principal instead of HttpContext.Current.User and Thread.CurrentPrincipal. The current code is fine for web host and the legacy self host. But for OWIN host, it won't always be right. If HttpRequestContext.Principal is a new abstraction that hides the host-specific details for you; it will make sure the principal is set the right way regardless of host. So instead of: Thread.CurrentPrincipal = principal; if (HttpContext.Current != null) HttpContext.Current.User = principal; Do: request.GetRequestContext().Principal = principal; Second: For new code, I'd recommend using authentication filters rather than message handlers for doing authentication. Authentication filters are a higher-level abstraction and that take care of some of the details for you, like setting the principal on the host and multiple authentication mechanisms cooperate together. To plug in an authentication filter, you can do one of three things: a. Add the filter globally (for all controllers/actions), like: config.Filters.Add(new ApiKeyAuthenticationFilter()); b. Add the filter to one specific controller (all of its actions) via an attribute, like: [ApiKeyAuthenticationFilter] public class TestController : ApiController { /* ... */ } c. Add the filter to one specific action via an attribute, like: public class TestController : ApiController { [ApiKeyAuthenticationFilter] public IEnumerable<string> Get() { /* ... */ } } Note that you'll need to have your filter implementation inherit from Attribute if you want to use options b or c. Below is a sample authentication filter for the ApiKey scheme above (for global use only; it doesn't inherit from Attribute). public class ApiKeyAuthenticationFilter : IAuthenticationFilter { public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) { HttpRequestMessage request = context.Request; string apiKey = request.GetQueryNameValuePairs().FirstOrDefault(k => k.Key == "apiKey").Value; if (string.IsNullOrWhiteSpace(apiKey)) { // Return indicating no authentication was attempted (for this authentication mechanism). return Task.FromResult(0); } string[] token = apiKey.Split('*'); if (token.Length != 2) { // Return indicating authentication was attempted but failed. context.ErrorResult = new BadRequestTextPlainResult { Content = "The apiKey query string parameter must be in the format x*y.", Request = request }; return Task.FromResult(0); } var claims = new List<Claim>{ new Claim(ClaimTypes.Name, token[1]), new Claim(ClaimTypes.NameIdentifier, token[0]) }; var principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") }); // Return indicating authentication was attempted and succeeded. context.Principal = principal; return Task.FromResult(0); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { // Nothing to do for this authentication mechanism since it is query string based. A mechanism that uses // the WWW-Authenticate header would update the result to add its WWW-Authenticate challenge; // something like context.Result = new AddChallengeOn401Result(context.Result); return Task.FromResult(0); } public bool AllowMultiple { get { return false; } } private class BadRequestTextPlainResult : IHttpActionResult { public string Content { get; set; } public HttpRequestMessage Request { get; set; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.BadRequest); response.Content = new StringContent(Content); response.RequestMessage = Request; return Task.FromResult(response); } } }