Asp.net MVC4: Authorize on both controller and action

77,752

Solution 1

You asked:

If I have Authorize attribute on both controller and action, which one will take the effect? Both?

To answer this simply: Both. The effect is to AND the two restrictions together. I'll explain why below ...

Details

So, there are a few reasons you could be asking this.

  1. You want to know how to enforce an additional constraint on an Action compared to a method. e.g.
    • At controller level, enforce users in the role "user"
    • At an action level, additionally enforce users in the role "admin"
  2. You want to replace the controller constraint at the action level
  3. You want to remove the controller constraint at the action level and make the method available to anonymous users

You didn't specify your MVC version, so I will assume the latest as of today (MVC 4.5). However, that won't change of the answer much even if you were using MVC 3.

[Anonymous] overrides controller [Authorize] (case 3)

Case 3. I don't need to cover (the use of [AllowAnonymous]) as it has been answered all over SO and all over the web already. Suffice to say: if you specify [AllowAnonymous] on an action it will make that action public even if the controller has [Authorize] on it.

You can also make an entire website subject to authorisation by using a global filter, and use AllowAnonymous on the few actions or controllers you want to make public.

[Authorize] is additive (case 1)

Case 1 is easy. Take the following controller as an example:

[Authorize(Roles="user")]
public class HomeController : Controller {
    public ActionResult AllUsersIndex() {
        return View();
    }

    [Authorize(Roles = "admin")]
    public ActionResult AdminUsersIndex() {
        return View();
    }
}

By default [Authorize(Roles="user")] makes all Actions in the Controller available to accounts in the "user" role only. Therefore to access AllUsersIndex you must be in the "user" role. However to access AdminUsersIndex you must be both in the "user" and the "admin" role. For example:

  • UserName: Bob, Roles: user, cannot access AdminUsersIndex, but can access AllUsersIndex
  • UserName: Jane, Roles: admin, cannot access AdminUsersIndex or AllUsersIndex
  • UserName: Tim, Roles: user & admin, can access AdminUsersIndex and AllUsersIndex

This illustrates that the [Authorize] attribute is additive. This is also true of the Users property of the attribute, which can be combined with Roles to make it even more restrictive.

This behaviour is due to the way that controller and action attributes work. The attributes are chained together and applied in the order controller then action. If the first one refuses authorization, then control returns and the action's attribute is not called. If the first one passes authorization, then the second one is then checked as well. You can override this order by specifying Order (for example [Authorize(Roles = "user", Order = 2)]).

Overriding [Authorize] (case 2)

Case 2 is trickier. Recall from above that the [Authorize] attributes are examined in the order (Global then) Controller then Action. The first one to detect that the user is ineligible to be authorized wins, the others don't get called.

One way around this is to define two new attributes as below. The [OverrideAuthorize] does nothing other than defer to [Authorize]; its only purpose is to define a type that we can check for. The [DefaultAuthorize] allows us to check to see if the Action being called in the request is decorated with a [OverrideAuthorize]. If it is then we defer to the Action authorization check, otherwise we proceed with the Controller level check.

public class DefaultAuthorizeAttribute : AuthorizeAttribute {
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        var action = filterContext.ActionDescriptor;
        if (action.IsDefined(typeof(OverrideAuthorizeAttribute), true)) return;

        base.OnAuthorization(filterContext);
    }
}
public class OverrideAuthorizeAttribute : AuthorizeAttribute {
    public override void OnAuthorization(AuthorizationContext filterContext)
    {
        base.OnAuthorization(filterContext);
    }
}

We can then use it like this:

[DefaultAuthorize(Roles="user")]
public class HomeController : Controller {
    // Available to accounts in the "user" role
    public ActionResult AllUsersIndex() {
        return View();
    }
    // Available only to accounts both in the "user" and "admin" role
    [Authorize(Roles = "admin")]
    public ActionResult AdminUsersIndex() {
        return View();
    }
    // Available to accounts in the "superuser" role even if not in "user" role
    [OverrideAuthorize(Roles = "superuser")]
    public ActionResult SuperusersIndex() {
        return View();
    }
}

In the above example SuperusersIndex is available to an account that has the "superuser" role, even if it does not have the "user" role.

Solution 2

I would like to add something to Overriding [Authorize] (case 2)

OverrideAuthorizeAttribute and DefaultAuthorizeAttribute works fine, but I discover that you can also use OverrideAuthorizationAttribute which overrides authorization filters defined at a higher level.

[Authorize(Roles="user")]
public class HomeController : Controller {
    // Available to accounts in the "user" role
    public ActionResult AllUsersIndex() {
        return View();
    }
    // Available only to accounts both in the "user" and "admin" role
    [Authorize(Roles = "admin")]
    public ActionResult AdminUsersIndex() {
        return View();
    }
    // Available to accounts in the "superuser" role even if not in "user" role
    [OverrideAuthorization()]
    [Authorize(Roles = "superuser")]
    public ActionResult SuperusersIndex() {
        return View();
    }
}

Solution 3

If use it on controller then, all methods of this controller will effected.

[Authorize]
public class SomeController(){

    // all actions are effected
    public ActionResult Action1
    public ActionResult Action2

If you want to prevent for one of these actions, you can use something like this:

[Authorize]
public class SomeController(){

    // all actions are effected
    public ActionResult Action1
    public ActionResult Action2

    [AllowAnonymous]
    public ActionResult Action3 // only this method is not effected...

Solution 4

I made an adaptation of this answer's second case for ASP.NET Core 2.1.

The difference with ASP.NET Core's AuthorizeAttribute is that you don't have to call AuthorizeAttribute.OnAuthorization base method to proceed to normal authorization. This means that even if you don't explicitly call the base method, the base AuthorizeAttribute could still short-circuit authorization by forbidding access.

What I did is that I created a DefaultAuthorizeAttribute that does not inherit from AuthorizeAttribute, but from Attribute instead. Since the DefaultAuthorizeAttribute does not inherit from AuthorizeAttribute, I had to recreate the authorization behavior.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class DefaultAuthorizeAttribute : Attribute, IAuthorizationFilter
{
    private readonly AuthorizeFilter m_authorizeFilter;

    public DefaultAuthorizeAttribute(params string[] authenticationSchemes)
    {
        var policyBuilder = new AuthorizationPolicyBuilder()
            .AddAuthenticationSchemes(authenticationSchemes)
            .RequireAuthenticatedUser();
        m_authorizeFilter = new AuthorizeFilter(policyBuilder.Build());
    }

    public void OnAuthorization(AuthorizationFilterContext filterContext)
    {
        if (filterContext.ActionDescriptor is ControllerActionDescriptor controllerAction
            && controllerAction.MethodInfo.GetCustomAttributes(typeof(OverrideAuthorizeAttribute), true).Any())
        {
            return;
        }
        m_authorizeFilter.OnAuthorizationAsync(filterContext).Wait();
    }
}

public class OverrideAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext filterContext) { }
}
Share:
77,752

Related videos on Youtube

frank
Author by

frank

Updated on July 05, 2022

Comments

  • frank
    frank almost 2 years

    If I have the Authorize attribute on both the controller and the action, which one will take the effect? Or will both take effect?

  • Andy Brown
    Andy Brown almost 11 years
    The OP asked what happens if you put [Authorize] on both the controller and the action "which one will take the effect? Both?", you haven't answered that.
  • AliRıza Adıyahşi
    AliRıza Adıyahşi almost 11 years
    @Andy Brown, So, If he define it on controller, then no need to write it on any action of this controller. unnecessary to use it on an action. You can understand from my answer. I mean no need to explain this, too.
  • Nick N.
    Nick N. almost 11 years
    I think if you read the answer clearly, he has answered it. Since he says that all methods will be affected, which implies that it doesn't matter if you put it on an action aswell.
  • Andy Brown
    Andy Brown almost 11 years
    @NickN. My point is that it can matter. Authorize attributes chain together. If the first one allows the user, the second can still block the user. Also the question is about two Authorize attributes (one on the controller, one on the action), not Authorize then AllowAnonymous. I have added another answer to illustrate my point.
  • Nick N.
    Nick N. almost 11 years
    @AndyBrown I agree with you now I have seen your answer.
  • AliRıza Adıyahşi
    AliRıza Adıyahşi almost 11 years
    There is no difference with mine. There is an attribute scope. Authorization should move to the first dam. This is same as comparison logic like if(condition1&&condition2) if first condition is false, then compailer not look second condition and skip if-block.
  • frank
    frank almost 11 years
    Thank you for the very detail information, this is what I want to learn when I asked this question. Thanks!
  • Ashkan
    Ashkan about 10 years
    Does "User" roles have access to SuperusersIndex action method?
  • Andy Brown
    Andy Brown about 10 years
    No. The OverrideAuthorize completely replaces the default permissions defined by DefaultAuthorize by virtue of the if (action.IsDefined(typeof(OverrideAuthorizeAttribute), true)) return; in DefaultAuthorizeAttribute.OnAuthorization
  • wirble
    wirble almost 9 years
    @Andy Brown This seems to work only in this case. If I defined controler [DefaultAuthorize(Roles="Publisher")] then actions [OverrideAuthorize(Roles = "Administrator,Editor,Reporter,Viewer")] ; it works as long as user is in the "action" group. But if user is in the controller group "Publisher", access is denied. It seems to ignore the Publisher group or something. Do you get this behavior?
  • Rahul Nikate
    Rahul Nikate almost 9 years
    @AndyBrown Thank you for detail explanation.
  • Bpainter
    Bpainter almost 8 years
    In MVC5, case 2 can be done simply by adding the [OverrideAuthorization] attribute to the action as described in this answer.
  • David Liang
    David Liang about 7 years
    But this doesn't work with Custom AuthorizeAttribute right?
  • Chuck Kasabula
    Chuck Kasabula about 6 years
    Requires MVC 5.
  • Jan Palas
    Jan Palas almost 5 years
    This does not work. Just look into implementation of OnAuthorizationAsync - there GetEffectivePolicyAsync always returns null because GetEffectivePolicyAsync only works with filters that are actually applied to the controller/action which our authorize filter is not. We only keep it in m_authorizeFilter...
  • Craig Brett
    Craig Brett over 4 years
    It helped me out. But please add that it's MVC5 in the answer in case people miss that.
  • The Red Pea
    The Red Pea about 2 years
    I was using Http AuthorizeAttribute, but Andy's example here uses Mvc's AuthorizeAttribute - more on the difference, here