I've got an MVC3 application with 4 levels of authentication, and 4 base controllers that tie to each one:
- Unauthenticated -
BaseController
- User -
BaseAuthController : BaseController
- Advisor -
BaseAdvisorController : BaseAuthController
- Admin -
BaseAdminController : BaseAuthController
Right now I have a series of overrides in place for special cases... e.g. a controller that is typically only for admins can have an action method or two that advisors can use... I have the overrides defined as strings in an array.
public class BaseAuthController : BaseController
{
/// <summary>
/// Enter action names in here to have them ignored during login detection
/// </summary>
public string[] NoAuthActions = new string[] { };
/// <summary>
/// Actions only usable by Users+
/// </summary>
public string[] UserOnlyActions = new string[] { };
/// <summary>
/// Actions only usable by Advisors+
/// </summary>
public string[] AdvisorOnlyActions = new string[] { };
/// <summary>
/// Actions only usable by Admins+
/// </summary>
public string[] AdminOnlyActions = new string[] { };
.......
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
//special code here to determine what to do with requested action...
//verifies that user is logged in and meets requirements for method...
//if not, redirects out to another page...
}
}
At the controller level I have them defined like this...
public class GrowerController : BaseAdminController
{
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
UserOnlyActions = new string[] { "GrowthStageSelection" };
AdvisorOnlyActions = new string[] { "Landing", "SeedSelection", "UpdateProjection",
"NitrogenApplications", "DeleteNitrogen", "MassUpload",
"VerifyHolding", "ConfirmHolding", "DeleteHoldingDir", "DeleteHoldingFile" };
base.OnActionExecuting(filterContext);
}
//......
[HttpPost]
public ActionResult GrowthStageSelection(int growerID, int reportGrowthStageID = 0)
{
//code...
}
}
This system has actually worked out pretty well for us, but the problem for me has been that it feels messy. You have to define the methods one place, and override their authentication level elsewhere if necessary. If you change the method name you have to remember to change it elsewhere.
What I'd LOVE to be able to do is decorate the methods themselves with authentication specific attributes and do away the string-based definitions (or at least make them transparent and use List<string>
dynamically or something). Here's an example of what I'm looking for...
[HttpPost]
[AdvisorAuthentication]
public ActionResult GrowthStageSelection(int growerID, int reportGrowthStageID = 0)
{
//code...
}
Problem is that I can't find a good way to achieve this with attributes. I've tried creating subclasses of ActionFilterAttribute
but they run after my BaseAuthController
's override for OnActionExecuting
. At that point it's too late in the game to add new methods to the string lists dynamically, and moreover I can't even seem to access the current controller instance from the attributes.
Maybe this whole idea is off base. Can anyone point me in the right direction? Thanks.
Final solution
First, I went ahead and deleted all of my special controllers except for BaseController - I had no use for them anymore. I moved the current special authentication code from BaseAuthController
into BaseController
. Next, I defined a series of attributes for each of my authentication states:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class BaseAuthAttribute : Attribute
{
public AuthLevels AuthLevel { get; protected set; }
public BaseAuthAttribute(AuthLevels level)
{
this.AuthLevel = level;
}
public override string ToString()
{
return string.Format("Auth Required: {0}", this.AuthLevel.ToString());
}
}
public class UnauthenticatedAccess : BaseAuthAttribute
{
public UnauthenticatedAccess()
: base(AuthLevels.Unauthenticated)
{
}
}
public class UserAccess : BaseAuthAttribute
{
public UserAccess()
: base(AuthLevels.User)
{
}
}
public class AdvisorAccess : BaseAuthAttribute
{
public AdvisorAccess()
: base(AuthLevels.Advisor)
{
}
}
public class AdminAccess : BaseAuthAttribute
{
public AdminAccess()
: base(AuthLevels.Admin)
{
}
}
Then in my BaseController
I modified the OnActionExecuting
to check the current auth level of the logged in user (if any) against the attribute. This is much cleaner than it was before! (Note: SessionUser
and AuthLevels
are custom objects for our project - you won't have those)
public partial class BaseController : Controller
{
/// <summary>
/// Override security at higher levels
/// </summary>
protected bool SecurityOverride = false;
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
BaseAuthAttribute authAttribute = filterContext.ActionDescriptor.GetCustomAttributes(false).OfType<BaseAuthAttribute>().FirstOrDefault();
if (authAttribute == null) //Try to get attribute from controller
authAttribute = filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes(false).OfType<BaseAuthAttribute>().FirstOrDefault();
if (authAttribute == null) //Fallback to default
authAttribute = new UnauthenticatedAccess(); //By default, no auth is required for base controller
if (!SessionUser.LoggedIn
&& authAttribute.AuthLevel == AuthLevels.Unauthenticated)
{
SecurityOverride = true;
}
else if (SessionUser.LoggedIn
&& SessionUser.LoggedInUser.AuthLevel >= (int)authAttribute.AuthLevel)
{
SecurityOverride = true;
}
if (!SessionUser.LoggedIn && !SecurityOverride)
{
//Send to auth page here...
return;
}
else if (!SecurityOverride)
{
//Send somewhere else - the user does not have access to this
return;
}
base.OnActionExecuting(filterContext);
}
// ... other code ...
}
That's it! Now just put it to use like so...
[AdminAccess]
public class GrowerController : BaseController
{
public ActionResult Index()
{
//This method will require admin access (as defined for controller)
return View();
}
[AdvisorAccess]
public ActionResult Landing()
{
//This method is overridden for advisor access or greater
return View();
}
}