Basic Auth with a Web API 2 IAuthenticationFilter
February 27, 2014 -MVC5/Web API 2 introduced a new IAuthenticationFilter (as opposed the the IAuthorizationFilter we needed to dual-purpose in the past), as well as a substantial overhaul of the user model with ASP.NET Identity. Unfortunately, the documentation is abysmal, and all the blog articles focus on the System.Web.Mvc.Filters.IAuthenticationFilter
, not the System.Web.Http.Filters.IAuthenticationFilter
, which is clearly something entirely different.
We had a project where we needed to support a Basic-over-SSL authentication scheme on the ApiControllers for a mobile client, as well as Forms auth for the MVC controllers running the admin interface. We were keen to leverage the new Identity model, mostly as it appears to be a much more coherent design than the legacy hodgepodge we’d used previously. This required a fair bit of decompilation and digging, but I eventually came up with something that worked.
Below is an excerpt of the relevant parts of our BasicAuthFilter
class - it authenticates against a UserManager<T>
(which could be the default EF version) and creates a (role-less) ClaimsPrincipal
if successful.
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken) {
var authHeader = context.Request.Headers.Authorization;
if (authHeader == null || authHeader.Scheme != "Basic")
context.ErrorResult = Unauthorized(context.Request);
else {
string[] credentials = ASCIIEncoding.ASCII.GetString(Convert.FromBase64String(authHeader.Parameter)).Split(':');
if (credentials.Length == 2) {
using (var userManager = CreateUserManager()) {
var user = await userManager.FindAsync(credentials[0], credentials[1]);
if (user != null) {
var identity = await userManager.CreateIdentityAsync(user, "BasicAuth");
context.Principal = new ClaimsPrincipal(new ClaimsIdentity[] { identity });
}
else
context.ErrorResult = Unauthorized(context.Request);
}
}
else
context.ErrorResult = Unauthorized(context.Request);
}
}
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) {
context.Result = new AddBasicChallengeResult(context.Result, realm);
return Task.FromResult(0);
}
private class AddBasicChallengeResult : IHttpActionResult {
private IHttpActionResult innerResult;
private string realm;
public AddBasicChallengeResult(IHttpActionResult innerResult, string realm) {
this.innerResult = innerResult;
this.realm = realm;
}
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) {
var response = await innerResult.ExecuteAsync(cancellationToken);
if (response.StatusCode == HttpStatusCode.Unauthorized)
response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic", String.Format("realm=\"{0}\"", realm)));
return response;
}
}
Note that you’ll need to use config.SuppressDefaultHostAuthentication()
in your WebApiConfig
in order to prevent redirection from unauthorised API calls.