3
votes

My current project is an internal web application built using ASP.Net MVC which I am adding authentication to. I have a pre-built HTTPModule which creates a IPrincipal with the appropriate roles. If the user isn't authenticated I get a user object with the role "Public"

As this is an internal application most of the pages are private and only viewable to the role "Admin". As I have a base controller I can do this:

[Authorize(Roles="Admin")]
public abstract class MyControllerBase : Controller
{
    ...
}

I have a problem though as some of the actions are viewable on a public website and if I attribute them like so:

[Authorize(Roles="Public")]
public class LoginController : MyController
{
    public ActionResult Index()
    {

    }
}

The page fails to load as the user isn't authenticated. It would seem the Role of "Public is being ignored on the inherited class. Does anyone know if the roles can be overridden by inherited classes?

I am also trying to avoid attributing all the controllers with Roles="Admin"

Thanks, Keith.

2

2 Answers

6
votes

You can derive a new attribute from AuthorizeAttribute and override the OnAuthorization method, then apply your customized attribute instead of Authorize. Below is the OnAuthorization method from one of my customized attributes that redirects to an error page if the privileges aren't sufficient instead of redirecting to the logon page.

I'm not sure exactly what this will buy you, though. When you decorate your class with the attribute, presumably you'll have to allow both Admin and Public (so who are you restricting since Public is anyone who is not authenticated?). You'd then have to decorate each of the controller methods that need to be restricted to Admin individually since the class attribute would allow access otherwise. You can achieve this behavior with the regular Authorize attribute by simply decorating just those non-publicly available methods (or classes that have no publicly available methods).

I suppose you could have your attribute check to see if the method being called is also decorated with the attribute and simply approve the authorization, which would effectively defer the authorization to the method level. You'd probably have to peek into the RouteData on the AuthorizationContext to get the action and use reflection to try and find the appropriate method based on parameters and request type.

    public override void OnAuthorization( AuthorizationContext filterContext )
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException( "filterContext" );
        }

        if (AuthorizeCore( filterContext.HttpContext ))
        {
            SetCachePolicy( filterContext );
        }
        else if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
        {
            // auth failed, redirect to login page
            filterContext.Result = new HttpUnauthorizedResult();
        }
        else
        {
            ViewDataDictionary viewData = new ViewDataDictionary();
            viewData.Add( "Message", "You do not have sufficient privileges for this operation." );
            filterContext.Result = new ViewResult { MasterName = this.MasterName, ViewName = this.ViewName, ViewData = viewData };
        }

    }

    protected void SetCachePolicy( AuthorizationContext filterContext )
    {
        // ** IMPORTANT **
        // Since we're performing authorization at the action level, the authorization code runs
        // after the output caching module. In the worst case this could allow an authorized user
        // to cause the page to be cached, then an unauthorized user would later be served the
        // cached page. We work around this by telling proxies not to cache the sensitive page,
        // then we hook our custom authorization code into the caching mechanism so that we have
        // the final say on whether a page should be served from the cache.
        HttpCachePolicyBase cachePolicy = filterContext.HttpContext.Response.Cache;
        cachePolicy.SetProxyMaxAge( new TimeSpan( 0 ) );
        cachePolicy.AddValidationCallback( CacheValidateHandler, null /* data */);
    }
5
votes

Well in the end I think my answer was in the question. Instead of putting the Authorize attribute on my base controller I have derived a new AdminBaseController.

[HandleError]
public abstract class MyControllerBase : Controller
{
    ...
}

[Authorize(Roles="Admin")]
public abstract class AdminControllerBase : MyControllerBase
{
    ....
}

Now any controllers that require authentication can derive from AdminControllerBase while my public controllers can derive from MyControllerBase. OO to the rescue.