Search code examples
asp.netasp.net-web-apiauthorize-attributeiauthorizationfilterasp.net-web-api-filters

How to rewrite code to use IAuthorizationFilter with dependency injection instead of AuthorizeAttribute with service location in Asp Net Web Api?


I have the custom AuthorizeAttribute where I need to use one of the business layer services to validate some data in the database before giving user a permission to view the resource. In order to be able to allocate this service within the my AuthorizeAttribute I decided to use service location "anti-pattern", this is the code:

internal class AuthorizeGetGroupByIdAttribute : AuthorizeAttribute
{
    private readonly IUserGroupService _userGroupService;

    public AuthorizeGetGroupByIdAttribute()
    {
        _userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
    }

    //In this method I'm validating whether the user is a member of a group. 
    //If they are not they won't get a permission to view the resource, which is decorated with this attribute.
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
        int groupId = int.Parse(parameters["groupId"]);
        int currentUserId = HttpContext.Current.User.Identity.GetUserId();

        return _userGroupService.IsUserInGroup(currentUserId, groupId);
    }

    protected override void HandleUnauthorizedRequest(HttpActionContext actionContex)
    {
        if (!HttpContext.Current.User.Identity.IsAuthenticated)
        {
            base.HandleUnauthorizedRequest(actionContex);
        }
        else
        {
            actionContex.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
        }
    }
}

I have couple of other attributes like this in my application. Using service locator is probably not a good approach. After searching the web a little bit I found some people suggesting to use IAuthorizationFilter with dependency injection instead. But I don't know how to write this kind of IAuthorizationFilter. Can you help me writing IAuthorizationFilter that will do the same thing that the AuthorizeAttribute above?


Solution

  • So after struggling for a while I think I managed to resolve this issue. Here are the steps you have to do in order to that:

    1) First you have to make GetGroupByIdAttribute passive, and by passive I mean an empty attribute without any logic within it (it will be used strictly for decoration purposes)

    public class GetGroupByIdAttribute : Attribute
    {
    }
    

    2) Then you have to mark a controller method, for which you want to add authorization, with this attribute.

    [HttpPost]
    [GetGroupById]
    public IHttpActionResult GetGroupById(int groupId)
    {
        //Some code
    }
    

    3) In order to write your own IAuthorizationFilter you have to implement its method ExecuteAuthorizationFilterAsync. Here is the full class (I included comments to guide you through the code):

    public class GetGroupByIdAuthorizationFilter : IAuthorizationFilter
    {
        public bool AllowMultiple { get; set; }
    
        private readonly IUserGroupService _userGroupService;
    
        //As you can see I'm using a constructor injection here
        public GetGroupByIdAuthorizationFilter(IUserGroupService userGroupService)
        {
            _userGroupService = userGroupService;
        }
    
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            //First I check whether the method is marked with the attribute, if it is then check whether the current user has a permission to use this method
            if (actionContext.ActionDescriptor.GetCustomAttributes<GetGroupByIdAttribute>().SingleOrDefault() != null)
            {
                Dictionary<string, string> parameters = actionContext.Request.GetQueryNameValuePairs().ToDictionary(x => x.Key, x => x.Value);
                int groupId = int.Parse(parameters["groupId"]);
                int currentUserId = HttpContext.Current.User.Identity.GetUserId();
    
                //If the user is not allowed to view view the resource, then return 403 status code forbidden
                if (!_userGroupService.IsUserInGroup(currentUserId, groupId))
                {
                    return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Forbidden));
                }
            }
            //If this line was reached it means the user is allowed to use this method, so just return continuation() which basically means continue processing 
            return continuation();
        }
    }
    

    4) The last step is to register your filter in the WebApiConfig.

    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Here I am registering Dependency Resolver
            config.DependencyResolver = ServiceLocator.Instance.DependencyResolver;
    
            //Then I resolve the service I want to use (which should be fine because this is basically the start of the application)
            var userGroupService = ServiceLocator.Instance.Resolve<IUserGroupService>();
    
            //And finally I'm registering the IAuthorizationFilter I created 
            config.Filters.Add(new GetGroupByIdAuthorizationFilter(userGroupService));
    
            // Web API routes
            config.MapHttpAttributeRoutes();
    
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{action}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );            
        }
    }
    

    Now, if needed, I can create additional IActionFilters that use IUserGroupService and then inject this service at the start of the application, from WebApiConfig class, into all filters.