Basic Auth with a Web API 2 IAuthenticationFilter

February 27, 2014 - asp-net-mvc authentication web-api

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.