WebAPI Authorization in Views and Unit Tests

Home / WebAPI Authorization in Views and Unit Tests

In my .NET 4.5 MVC projects, I already have helper methods/extensions that let me determine if a user has access to a particular controller action. This provides a nice mechanism to hide and show buttons, hyperlinks, or other UI action elements based on the authorization attributes that have been defined on the MVC Controller.

However, this is a bit more complicated with ApiControllers.


In order to check whether a user has access to an ApiController action, we effectively have to create the entire request and its route data, and then apply any authorization action filter attributes that have been put on the controller or action. This differs greatly from the MVC Controllers since MVC Controllers utilize an AuthorizationContext that you can simply test against. One good thing, though, is that once the ApiController code is in place, it can also be used to mock and test ApiController routes and their authorization.

Why even bother with this? Well, I find that this provides more consistency, makes the authorization succinct, and we aren’t repepeating ourselves by perform checks with User.IsInRole() and such that mimic what our authorize attributes are already doing.

Primarily, since I’m going to utilize this method within Razor views for hiding buttons, that are subsequently tied to ApiController actions, I created a simple HtmlHelper extension:

/// <summary>
/// Checks user security permissions.  If allowed, return true.  Otherwise return false.
/// </summary>
/// <returns>Link or specified text.</returns>
public static bool IsAuthorizedApi(this HtmlHelper htmlHelper, string apiEndpoint, HttpMethod method)
{
    var principal = htmlHelper.ViewContext.HttpContext.User;
    var url = string.Format("{0}/{1}", htmlHelper.ViewContext.HttpContext.Request.Url.GetLeftPart(UriPartial.Authority), apiEndpoint);
    return CheckAccess(principal, method, url);
}

The helper will look at the current HttpContext and Request to get the base Uri and IPrincipal. These are passed to a private method which will then perform all of the magic.

private static bool CheckAccess(IPrincipal principal, HttpMethod method, string url, HttpConfiguration config = null)
{
    var hasAccess = true;
    try
    {
        // Use the default configuration if one wasn't passed in - this will honor any IHttpControllerSelector that has been injected
        config = config ?? GlobalConfiguration.Configuration;
        var selector = (System.Web.Http.Dispatcher.IHttpControllerSelector)config.Services.GetService(typeof(System.Web.Http.Dispatcher.IHttpControllerSelector));

        // Generate a request with route data.  Route data will contain information about the path such as area/controller/action
        var request = new HttpRequestMessage(method, url);
        var routeData = config.Routes.GetRouteData(request);
        request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpRouteDataKey] = routeData;

        // Create a controllerContext that we will use with the action and set the descriptor as needed.  Setting the principal is also needed as this isn't
        // attached by default.
        var controllerContext = new HttpControllerContext(config, routeData, request);
        controllerContext.RequestContext.Principal = principal;
        var controllerDescriptor = selector.SelectController(request);
        controllerContext.ControllerDescriptor = controllerDescriptor;

        // Now get a handle to the action method and create a context for it
        var actionSelector = new ApiControllerActionSelector();
        var actionDescriptor = actionSelector.SelectAction(controllerContext);
        var actionContext = new HttpActionContext(controllerContext, actionDescriptor);

        // Here's the odd part.  By default, getting an action method and a context doesn't appear to take into account action filters.
        // So, we'll get these attributes off of both the controller and the action and execute them
        var controllerFilters = controllerDescriptor.ControllerType.GetCustomAttributes(typeof(System.Web.Http.AuthorizeAttribute), false).Select(x => (System.Web.Http.AuthorizeAttribute)x).ToList();
        var actionFilters = actionDescriptor.GetCustomAttributes<System.Web.Http.AuthorizeAttribute>().ToList();
        var filters = new List<System.Web.Http.AuthorizeAttribute>();
        filters.AddRange(controllerFilters);
        filters.AddRange(actionFilters);

        // Iterate over each filter executing its OnAuthorizationMethod
        foreach (var filter in filters)
        {
            filter.OnAuthorization(actionContext);
        }

        // Get the authorization result from the actionExecutedContext.  Checking the status code lets us know if the user has access.  A null response indicates
        // the user was authorized as well.
        var actionExecutedContext = new HttpActionExecutedContext(actionContext, null);
        var statusCode = actionExecutedContext.Response == null ? HttpStatusCode.OK : actionExecutedContext.Response.StatusCode;
        if (statusCode == HttpStatusCode.Unauthorized || statusCode == HttpStatusCode.Forbidden)
        {
            hasAccess = false;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.InnerException);
    }

    return hasAccess;
}

I’ve commented the above to make it easy to see what is happening. The flow includes selecting a controller, generating a request/controllerContext, and then selecting the ApiControllerAction. Note that we have to attach the IPrinicipal to the RequestContext since this doesn’t happen automatically when the ControllerContext is instantiated. After we have a handle to the ApiController and ApiControllerAction, we can inspect these objects for AuthorizeAttributes. For each AuthorizeAttribute, we execute its OnAuthorization method. If the IPrinicipal has access, then a NULL response is returned on the HttpActionExecutedContext. If the IPrinicipal is denied access, we can instext the StatusCode on the Response.

All of this is wrapped in a Try/Catch because there is no validation for the specified route. If the route is not found, then the SelectController() method (iirc) will throw an exception.

With the HtmlHelper extension in place, it’s a simple matter to utilize it within our Razor view:

@{
    var canPerformAction = Html.IsAuthorizedApi("api/action", HttpMethod.Put);
}

<div class="buttons">
    @if (canPerformAction)
    {
        <button class="btn btn-default" ng-click="vm.performAction()">Perform Action</button>
    }
</div>

To utilize this code inside of unit test, we could instantiate an MVC Controller in order to retrieve an HtmlHelper and call the endpoint checker extension. However, it’s easier to utilize a separate method that doesn’t rely on an HtmlHelper:

public static bool IsAuthorizedApi(IPrincipal principal, HttpMethod method, string apiEndpoint)
{
    var url = string.Format("{0}/{1}", System.Web.HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority), apiEndpoint);
    return CheckAccess(principal, method, url);
}

Utilizing this method, we would only have to add our route in the unit test, which is pretty typically when unit testing controller endpoints. Where this differs from the HtmlHelper is that you have to create the HttpConfiguration object and add routes to it. Something along these lines would suffice for simply testing the security (where we don’t care about the route being correct):

var config = new System.Web.Http.HttpConfiguration();
System.Web.Http.HttpRouteCollectionExtensions.MapHttpRoute(config.Routes,
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional });

var identity = new ClaimsIdentity(new[] {
    new Claim(ClaimsIdentity.DefaultNameClaimType, "test.user")
}, "Application");

identity.AddClaim(new Claim(ClaimTypes.Role, "RoleToCheck"));
var isAuthorized = AuthorizedApiHtmlHelper.IsAuthorizedApi(identity as IPrincipal, System.Net.Http.HttpMethod.Post, "api/actionToPerform", config);

And that should cover most of the bases for the purposes of not only testing, but utilizing ApiController action security without our views.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.