Custom MVC Attributes

The native ASP.Net Membership provider has attributes that we can use to decorate Actions and set permissions. But you may want to extend this security so this article will describe how you can do that.

Let's say we have an administrative Controller called UsersController. In this controller, there is an Action called Index() which lists all the users in the system.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace MVCApp.Controllers
{
    public class UsersController : Controller
    {
        private readonly IViewModelUserBuilder _builder;

        public UsersController(IViewModelUserBuilder builder)
        {
            _builder = builder;
        }
		
        //
        // GET: /Admin/Users/
        public ActionResult Index()
        {
            var model = _builder.GetAllUsers();
            return View(model);
        }
	}
}

You might think this is fine, but think again. What happens if someone directly types into their browser http://mywebsite/users? They will get to see this view and all the users that are returned from the database in your action. Not good.

What you should do is decorate your action with the [Authorize] annotation. This will ensure that the user is authenticated before they can access this view.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace MVCApp.Controllers
{
    [Authorize]
    public class UsersController : Controller
    {
        private readonly IViewModelUserBuilder _builder;

        public UsersController(IViewModelUserBuilder builder)
        {
            _builder = builder;
        }
		
        //
        // GET: /Admin/Users/
        public ActionResult Index()
        {
            var model = _builder.GetAllUsers();
            return View(model);
        }
	}
}

But this still does not guarantee security. You may only want Administrators to see this list of users. If you are using the built-in .Net security model, can you extend the [Authorize] annotation to include security group names. The code below will only allows users in the "Administrators" group to see the content of this view.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace MVCApp.Controllers
{
    [Authorize("Administrator")]
    public class UsersController : Controller
    {
        private readonly IViewModelUserBuilder _builder;

        public UsersController(IViewModelUserBuilder builder)
        {
            _builder = builder;
        }
		
        //
        // GET: /Admin/Users/
        public ActionResult Index()
        {
            var model = _builder.GetAllUsers();
            return View(model);
        }
	}
}

But this requires that you use the built-in security model and I (generally) do not. I have my own builders and Membership methods that I use instead (I will share these methods with you if you want. Just let me know via the comments below). Instead, I customised the Authorize attribute to suit MY data store and builders.

Firstly, when my users log in, I have a Membership class that populates the session with (amongst other things) a list of the user's roles.

//Store the user roles in session
var roles = new List<ViewModelRole>();
var vm = _authBusiness.GetRolesByUserName(userName).ToList();
roles = Mapper.Map(vm, roles);

HttpContext.Current.Session[Constants.SESSION_USER_ROLES] = roles;

Now I can use these roles at any time in my application without having to hit the database again. The downside is that the list can become "stale". That is, if the user's roles change, the user needs to log off and back on again for those changes to be visible. A small price to pay, IMHO.

So now we have all the roles for the currently logged in user in session.

Next thing to do is override the AuthorizeAttribute class with our own code. We just create a new class something like this.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Mvc;

namespace MVCApp.Authentication
{	
    public class CustomAuthorize : AuthorizeAttribute
    {
    	//	CustomAuthorize will be the syntax we use as decoration on our Actions
        private readonly string[] _allowedroles;
        public CustomAuthorize(params string[] roles)
        {
        	//	this is the comma-delimited list of roles passed from the Authorize attribute
            _allowedroles = roles;
        }

        protected override bool AuthorizeCore(HttpContextBase httpContext)
        {
        	//	get the list of roles that we already have in session
            List<ViewModelRole> roles = (HttpContext.Current.Session["SESSION_USER_ROLES"]) as List<ViewModelRole>;
            //	if there are none, we already fail so true immediately
            if (roles == null || !roles.Any())
                return false;

			//	looks for matches between the list of roles in session and the list of roles passed from the Authorize attribute
            return _allowedroles.Select(role => roles.Where(x => x.Name == role)).Any(matchedRoles => matchedRoles.Any());
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
            {
            	//	is the user authenticated?
                base.HandleUnauthorizedRequest(filterContext);
            }
            else
            {
            	//	use is authenticated so throw an exception
		throw new HttpException((Int32)HttpStatusCode.Forbidden, "You do not have access to this page. Please contact your administrator.");
            }
        }
    }
}

Check the comments in the code for some more description, but we do is

  • Pass in a comma-delimited list of roles
  • Get the list of roles already stored in session
  • Compare the two lists and find a match

And then our Action decoration looks like this.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Script.Serialization;

namespace MVCApp.Controllers
{
    [CustomAuthorize("Administrator")]
    public class UsersController : Controller
    {
        private readonly IViewModelUserBuilder _builder;

        public UsersController(IViewModelUserBuilder builder)
        {
            _builder = builder;
        }
		
        //
        // GET: /Admin/Users/
        // Alternatively, you can set the decoration on the action
        // Just remember though, the controller decoration will take precedence over the action
        [CustomAuthorize("Administrator")]
        public ActionResult Index()
        {
            var model = _builder.GetAllUsers();
            return View(model);
        }
    }
}

Let me know if you would like to know any more about my Membership provider.

Til next time ...