0

I've got an MVC3 application with 4 levels of authentication, and 4 base controllers that tie to each one:

  1. Unauthenticated - BaseController
  2. User - BaseAuthController : BaseController
  3. Advisor - BaseAdvisorController : BaseAuthController
  4. 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();
    }
}
jocull
  • 20,008
  • 22
  • 105
  • 149
  • why do you want to have basecontroller if you are going to create different authentication filter as an attribute? – Pravin Pawar Aug 10 '12 at 05:48
  • BaseController stores a lot of common functionality (properties, methods) used all over the application in all of the different controllers. It's really important. – jocull Aug 10 '12 at 06:51

2 Answers2

1

If I understood your question properly, you can implement your own custom attributes (not authorisation attributes) and in the overriden OnActionExecuting of the base controller, you can retrieve the custom attributes of the executing method and based on wich ones are defined you can take appropriate actions. So if a method has the [AdvisorAuthentication] you know that you need to check for those credentials before proceeding.

EDIT: I don't have an example to point you to as this is something I have implemented in one of my projects. I have no access to that code now but here is an outline:

protected override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        base.OnActionExecuted(filterContext);
        IEnumerable<MyCustomAttribute> attributes = filterContext.ActionDescriptor.GetCustomAttributes(false).OfType<MyCustomAttribute>();
        foreach (MyCustomAttributeobj in attributes)
        {
            switch(MyCustomAttribute.AttribType){
                case MyCustomeAttribute.AdvisorAuthentication:

                    break;
                case MyCustomeAttribute.AdminAuthentication:

                    break;
            }
        }
    }

You can implement just one custom attribute MyCustomAttribute and have it accept a parameter to indicate which authorization type you want. Like that the use of the attribute becomes [MyCustomAttribute("MyCustomeAttribute.AdminAuthentication")]

JTMon
  • 3,189
  • 22
  • 24
  • That sounds like a great solution. Can you provide or link to a minor example? – jocull Aug 10 '12 at 20:52
  • I don't have a an example to point to but I have edited my response to include some pointers on how to proceed. If you need more clarifications let me know :) – JTMon Aug 10 '12 at 21:19
  • I ***LOVE*** this solution! I will post my solution above for reference. Thanks so much! – jocull Aug 11 '12 at 06:26
  • Glad it helped. Good luck with it :) – JTMon Aug 13 '12 at 06:17
0

You can create different Authorize attributes extending IAuthorizationFilter and FilterAttribute something like this

public sealed class AuthenticateAdvisorAttribute : IAuthorizationFilter, FilterAttribute
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        //advisor specific logic goes here
    }
}

public sealed class AuthenticateAdminAttribute : IAuthorizationFilter, FilterAttribute
{
    public void OnAuthorization(AuthorizationContext filterContext)
    {
        //admin specific logic goes here
    }
}

And then you can apply those attributes wherever you require to controller classes/actions as

[AuthenticateAdmin]
public class AdminController : Controller
{

}

[AuthenticateAdvisor]
public class AdvisorController : Controller
{

}
Pravin Pawar
  • 2,559
  • 3
  • 34
  • 40
  • What does `OnAuthorization(AuthorizationContext filterContext)` override? I'm not using Forms auth in this app, and I'm assuming that makes a difference. We have an object maintained in session that controls the auth. – jocull Aug 10 '12 at 06:50
  • It has nothing to do with what authentication you are using. This is the apporpriate place where u can have auth logic..BTW you can always access Session variables here using `filterContext.HttpContext.Session["key"]` – Pravin Pawar Aug 10 '12 at 07:00
  • Is there a way to get an instance of the current controller from this method? – jocull Aug 10 '12 at 07:23
  • I will have to try casting that as `BaseController`. Thanks. – jocull Aug 10 '12 at 20:53