Basic HTTP authentication in ASP.NET Web API using membership provider

In this blog post I am going to show how to provide Basic HTTP authentication in a Web API project by extending framework's AuthotrizeAttribute.

ASP.NET Web API is a great tool to create lightweight, HTTP-based APIs for your internet and mobile applications. In most scenarios you will need to provide some kind of authentication and authorization mechanism to restrict and isolate resources exposed by your services. Security in ASP.NET Web API is deferred to the hosting infrastructure. When running within IIS, authorization mechanism runs on top of an existing ASP.NET security system, meaning you can leverage existing features like ... good 'ol membership and role providers.

HTTP authentication

Out of the box ASP.NET provides two types of authentication that can be easily used within Web API:

  • forms authentication
  • windows authentication

For obvious reasons windows authentication is not an option with Internet exposed services. Forms authentication on the other hand is a mechanism that works well in interactive applications (eg. a website), but because of its cookie-based nature is not entirely REST friendly (as we need to be aware of the state) and requires clients to manage cookies on their side. Its not an issue when services are invoked from the browser (eg. using jQuery), as it will remember cookies for us, but may introduce another layer of complexity when consumed by mobile platforms.

HTTP authentication on the other hand is part of the standard protocol and can be easily handled by most popular client and mobile platforms.

Basic HTTP authentication sends credentials in plaintext (unencrypted) which means you must use secure transport layer (SSL) to provide encryption.

Creating ASP.NET Web API project

Let's start off by creating a new ASP.NET MVC 4 project and selecting Web API template for it. You may delete unnecessary Views, .css and .js files as we care only about bare minimum needed to host Web API services (Global.asax, Web.config and a controller).

Please note that currently (May 2012) Web API is still in a pre-release stage, which means breaking changes may be included in future releases. While building this example I have been using the latest source code available at the time of writing. To have the latest version you can use either git to clone the repository available at https://git01.codeplex.com/aspnetwebstack.git or use NuGet and nightly build packages provided (see this blog for more information).

Let's add a very simple model for the sake of example (it's not that important)

public class Book  
{
    public int Id{ get; set; }
    public string Author { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
}

System.Web.Http.Authorize filter

Next, create a simple controller. Any method would do, but GET will be the easiest to test. Web API ships with System.Web.Http.Authorize filter attribute that can be used to restrict access to certain actions or controllers (note that this is not System.Web.Mvc.AuthorizeAttribute from ASP.NET MVC, but its equivalent).

public class BooksController : ApiController  
{
    [Authorize]
    public IEnumerable<;Book>; Get()
    {
        var result = new List<;Book>;()
        {
            new Book()
            {
                Author = "John Fowles",
                Title = "The Magus",
                Description = "A major work of mounting tensions " +
                                "in which the human mind is the guinea-pig."
            },
            new Book()
            {
                Author = "Stanislaw Ulam",
                Title = "Adventures of a Mathematician",
                Description = "The autobiography of mathematician Stanislaw Ulam, " +
                                "one of the great scientific minds of the twentieth century."
            }
        };
        return result;
    }
}

Thats all you need to do to prevent anonymous users from reading your books! Because by default WebAPI project is configured to use forms authentication when you try to get the resource you will be redirected to a non-existent login url.

GET http://localhost:13791/api/books HTTP/1.1  
User-Agent: Fiddler  
Host: localhost:13791
HTTP/1.1 302 Found  
Server: ASP.NET Development Server/10.0.0.0  
Date: Sat, 12 May 2012 22:25:56 GMT  
X-AspNet-Version: 4.0.30319  
Location: /Account/Login?ReturnUrl=%2fapi%2fbooks  
Cache-Control: no-cache

ASP.NET will intercept unauthorized response (from Authorize attribute) and change it to 302 redirection response. To disable this behavior turn off forms authentication in your web.config file.

<;system.web>;  
  <;authentication mode="None">;
  <;/authentication>;
  <;!--(...)-->;
<;/system.web>;

;

Custom authorization filter

You may be tempted to implement HTTP Authentication in your controller. This is usually a bad idea, partially because of potential caching issues and partially because having authorization logic in controllers is a bad design. Jon Galloway explained it here for ASP.NET MVC. Deriving from Authorize filter is a potential way to customize the way ASP.NET MVC integrates with the underlying ASP.NET security system.

So let's try this approach and derive from AuthorizationAttribute and create a filter that will handle HTTP basic authentication.

/// <;summary>;  
/// HTTP authentication filter for ASP.NET Web API
/// <;/summary>;
public abstract class BasicHttpAuthorizeAttribute : AuthorizeAttribute  
{
    private const string BasicAuthResponseHeader = "WWW-Authenticate";
    private const string BasicAuthResponseHeaderValue = "Basic";

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (actionContext == null)
            throw Error.ArgumentNull("actionContext");
        if (AuthorizationDisabled(actionContext)
            || AuthorizeRequest(actionContext.ControllerContext.Request))
            return;
        this.HandleUnauthorizedRequest(actionContext);
    }

    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        if (actionContext == null)
            throw Error.ArgumentNull("actionContext");
        actionContext.Response = CreateUnauthorizedResponse(actionContext
            .ControllerContext.Request);
    }

    private HttpResponseMessage CreateUnauthorizedResponse(HttpRequestMessage request)
    {
        var result = new HttpResponseMessage()
        {
            StatusCode = HttpStatusCode.Unauthorized,
            RequestMessage = request
        };

        //we need to include WWW-Authenticate header in our response,
        //so our client knows we are using HTTP authentication
        result.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
        return result;
    }

    private static bool AuthorizationDisabled(HttpActionContext actionContext)
    {
        //support new AllowAnonymousAttribute
        if (!actionContext.ActionDescriptor
            .GetCustomAttributes<;AllowAnonymousAttribute>;().Any())
            return actionContext.ControllerContext
                .ControllerDescriptor
                .GetCustomAttributes<;AllowAnonymousAttribute>;().Any();
        else
            return true;
    }

    private bool AuthorizeRequest(HttpRequestMessage request)
    {
        AuthenticationHeaderValue authValue = request.Headers.Authorization;
        if (authValue == null || String.IsNullOrWhiteSpace(authValue.Parameter)
            || String.IsNullOrWhiteSpace(authValue.Scheme)
            || authValue.Scheme != BasicAuthResponseHeaderValue)
        {
            return false;
        }

        string[] parsedHeader = ParseAuthorizationHeader(authValue.Parameter);
        if (parsedHeader == null)
        {
            return false;
        }
        IPrincipal principal = null;
        if (TryCreatePrincipal(parsedHeader[0], parsedHeader[1], out principal))
        {
            HttpContext.Current.User = principal;
            return CheckRoles(principal) && CheckUsers(principal);
        }
        else
        {
            return false;
        }
    }

    private bool CheckUsers(IPrincipal principal)
    {
        string[] users = UsersSplit;
        if (users.Length == 0) return true;
        //NOTE: This is a case sensitive comparison
        return users.Any(u=>;principal.Identity.Name == u);
    }

    private bool CheckRoles(IPrincipal principal)
    {
        string[] roles = RolesSplit;
        if(roles.Length == 0) return true;
        return roles.Any(principal.IsInRole);
    }

    private string[] ParseAuthorizationHeader(string authHeader)
    {
        string[] credentials = Encoding.ASCII.GetString(Convert
                                                        .FromBase64String(authHeader))
                                                        .Split(
                                                        new[] { ':' });
        if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0])
            || string.IsNullOrEmpty(credentials[1])) return null;
        return credentials;
    }

    protected string[] RolesSplit
    {
        get { return SplitStrings(Roles); }
    }

    protected string[] UsersSplit
    {
        get { return SplitStrings(Users); }
    }

    protected static string[] SplitStrings(string input)
    {
        if(string.IsNullOrWhiteSpace(input)) return new string[0];
        var result = input.Split(',')
            .Where(s=>;!String.IsNullOrWhiteSpace(s.Trim()));
        return result.Select(s =>; s.Trim()).ToArray();
    }

    /// <;summary>;
    /// Implement to include authentication logic and create IPrincipal
    /// <;/summary>;
    protected abstract bool TryCreatePrincipal(string user, string password,
        out IPrincipal principal);
}

The most important part of the filter is its OnAuthorization(HttpActionContext actionContext) method that sets HTTP headers and HTTP response in case of unauthorized requests. Please note that we want developers be able to selectively disable authentication using AllowAnonymousAttribute - thats the logic handled by AuthorizationDisabled method.

The class itself is abstract and its concrete implementations should do actual checks against user datastore. Alternatively we could have used dependency injection and some kind of interface to abstract user retrieval.

Using membership and role provider

To add support for membership and role providers from our web.config we need to provide a simple BasicHttpAuthorizeAttribute implementation.

public class MembershipHttpAuthorizeAttribute : BasicHttpAuthorizeAttribute  
{
    /// <;summary>;
    /// Implement to include authentication logic and create IPrincipal
    /// <;/summary>;
    protected override bool TryCreatePrincipal(string user, string password,
        out IPrincipal principal)
    {
        principal = null;
        if (!Membership.Provider.ValidateUser(user, password))
            return false;
        string[] roles = System.Web.Security.Roles.Provider.GetRolesForUser(user);
        principal = new GenericPrincipal(new GenericIdentity(user), roles);
        return true;
    }
}

Of course you need to configure your providers in .config file as well as provide database to store your users.

Now you can decorate actions and controllers with the newly created attribute to use MembershipProvider-backed HTTP authentication with your service.

[MembershipHttpAuthorizeAttribute(Roles = "Reader")]  
public IEnumerable<;Book>; Get()

So is this approach ideal? Well, obviously not. Its better than having all authorization logic in controllers, but still its not perfect. We have coupled our controller with a concrete authentication method. Depending on your application needs it may or may not pose a problem to maintainability. In many small mobile apps that's probably not something to be too worried about. The solution to this flaw would be either to use dependency injection (and for example fluent security configuration) instead of abstract classes or to use custom ASP.NET HTTP module. But that's material for another blog post :)

Anyway HTTP Authentication + Web API seems like a very good combination to get a lightweight, standard compliant and secure service layer for mobile apps.

comments powered by Disqus