Basic Auth with a Web API 2 IAuthenticationFilter

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.

Advertisements

Implementing AWS authentication for your own REST API

If you need to build an authentication mechanism for an HTTP-based REST API, a common approach is to use HTTP Basic – it’s simple, all clients have it built-in, it’s easy to test from the browser, and you can store passwords as hashes. The downside is that your credentials are transmitted in (nearly) plain text, which makes SSL (with its associated security restrictions and computational cost) a necessity.

If you’d like to implement a simple scheme for a non-sensitive API that doesn’t require SSL, this is more complicated. HTTP Digest requires storing unhashed passwords on the server, and requires a challenge-response conversation between server & client. Schemes like Kerberos and three-legged oath require hanging your hat on a third-party authentication provider, and are awkward to implement in a client.

Luckily, in software if you hit a problem you can usually copy somebody else’s solution. This is what the Microsoft Azure team did when implementing their API authentication – “Let’s just copy what Amazon does“. Amazon probably copied someone else. Who am I to argue with that approach?

The general concept behind these schemes is relatively simple:

  1. Come up with a way to generate API & secret keys for a client. These are usually base64-encoded cryptographically generated random byte arrays.
  2. For each request, hash a string containing the requested URL and specific headers (including the Date header) with the secret key using HMAC-SHA1.
  3. Add an Authorize header with a custom scheme name (e.g. ‘AWS’), containing the access key & base64-encoded signature separated by a colon.

The client goes through this process to generate the Authorize header, then the server performs a reverse of the procedure using the stored secret key to authenticate the request. Additionally, the server checks the Date header value against server time to check for replay attacks.

Below is the source for an AuthorizeAttribute subclass (for an MVC3 REST API). The code is easily adaptable to other frameworks, such as an OpenRasta PipelineContributor. The injected Session property in this instance is an NHibernate Session, and the ApiKey class is mapped to a database table. Successful authentication adds a custom IPrincipal to the HttpContext. Note that none of the x-aws headers are being used.

public class ApiAuthenticateAttribute : AuthorizeAttribute
{    
    private static System.Text.UTF8Encoding utf8 = new System.Text.UTF8Encoding();
 
    [Inject]
    public ISession Session { private get; set; }

    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var request = filterContext.HttpContext.Request;
        IPrincipal principal = null;

        if (request.Headers["Authorization"] != null && request.Headers["Authorization"].StartsWith("AWS "))
        {
            // Amazon AWS authentication scheme.
            var credential = filterContext.HttpContext.Request.Headers["Authorization"].Substring(4).Split(':');
            var apiKey = Session.Query<ApiKey>().Where(k => k.AccessKey == credential[0]).FirstOrDefault();
            if (apiKey != null && !apiKey.IsDisabled && credential.Count() > 1)
            {
                // check the date header is present & within 15 mins
                DateTime clientDate;
                if (request.Headers["Date"] != null
                    && DateTime.TryParseExact(request.Headers["Date"], "R", DateTimeFormatInfo.CurrentInfo, DateTimeStyles.AdjustToUniversal, out clientDate)
                    && Math.Abs((clientDate - DateTime.UtcNow).TotalMinutes) <= 15)
                {
                    // build the signature & check for match
                    var stringToSign = String.Format("{0}\n{1}\n{2}\n{3}\n{4}",
                        request.HttpMethod,
                        request.Headers["Content-MD5"] ?? "",
                        request.Headers["Content-Type"] ?? "",
                        request.Headers["Date"] ?? "",
                        request.RawUrl);

                    var hmac = new HMACSHA1(utf8.GetBytes(apiKey.SecretKey));
                    var signature = Convert.ToBase64String(hmac.ComputeHash(utf8.GetBytes(stringToSign)));
                    if (signature == credential[1])
                    {
                        principal = apiKey.ToPrincipal();
                    }
                }
            }
        }

        if (principal == null)
        {
             filterContext.Result = new HttpUnauthorizedResult();
        }
        else
        {
            filterContext.HttpContext.User = principal;
        }
    }
}

Generating API Keys can be done like so:

public ApiKey()
{
    // Generate random keys by using RNGCryptoServiceProvider & Base64 encoding the output
    // Key lengths match AWS keys.
    var rngProvider = RNGCryptoServiceProvider.Create();
    var bytes = new byte[15];
    rngProvider.GetBytes(bytes);
    // Do some magic to ensure we have uppercase & digits only.
    AccessKey = Convert.ToBase64String(bytes).ToUpper().Replace("+", "0").Replace("/", "9");
    bytes = new byte[30];
    rngProvider.GetBytes(bytes);
    SecretKey = Convert.ToBase64String(bytes);
}

For the client, most languages have freely available Amazon client code that can be easily adapted. Reusing a popular scheme like this saves a lot of time & energy over rolling a completely custom solution, particularly where a number of disparate client platforms are likely to be used.