How can we set authorization for a whole area in ASP.NET MVC?

38,613

Solution 1

Web.config-based security should almost never be used in an MVC application. The reason for this is that multiple URLs can potentially hit a controller, and putting these checks in Web.config invariably misses something. Remember - controllers are not associated with areas, routes are associated with areas. The MVC controller factory will happily serve controllers from the Areas/ folder for non-area requests if there's no conflict.

For example, using the default project structure, adding an Admin area with an AdminDefaultController, you can hit this controller via /Admin/AdminDefault/Index and /AdminDefault/Index.

The only supported solution is to put your attribute on a controller base class and to ensure that each controller within the area subclasses that base class.

Solution 2

I have just been investigating this same issue. Since it is not possible to secure controllers based on areas, a simpler option comes to mind.

Create a base controller definition for each area that overrides Controller, and add the security require to this. Then you just have to ensure each controller in the area overrides AreaController instead of Controller. For example:

/// <summary>
/// Base controller for all Admin area
/// </summary>
[Authorize(Roles = "Admin")]
public abstract class AdminController : Controller { }

It does still require that you derive each controller in the Admin area from this base,

public class HomeController : AdminController
{
    // .. actions
}

but at least you have a single point where you define the security for the area.

Solution 3

I just started on this... but so far this is working pretty good for me.

I create a custom AuthorizeAttribute class and add this in the RegisterGlobalFilters function.

In CustomAuthorizeAttribute I check for various conditions based on the area it is in.

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new CustomAuthorizeAttribute());
        filters.Add(new HandleErrorAttribute());
    }
}

public class CustomAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var routeData = httpContext.Request.RequestContext.RouteData;
        var controller = routeData.GetRequiredString("controller");
        var action = routeData.GetRequiredString("action");
        var area = routeData.DataTokens["area"];
        var user = httpContext.User;
        if (area != null && area.ToString() == "Customer")
        {
            if (!user.Identity.IsAuthenticated)
                return false;
        }
        else if (area != null && area.ToString() == "Admin")
        {
            if (!user.Identity.IsAuthenticated)
                return false;
            if (!user.IsInRole("Admin"))
                return false;
        }
        return true;
    }
}

Solution 4

If all of your admin code is in one controller then add Authorize to the entire class.

[Authorize]
public class AdminController : Controller
{
     .......
}

Solution 5

The currently accepted answer is not the most secure solution because it requires the developer to always remember to inherit that new base class for any new controllers or actions ("blacklisting"; allowing users access to everything unless an action is manually restricted). This especially causes problems when new developers, unacquainted with your rituals, are introduced to the project. It is easy to forget to inherit the proper controller class if done that way, especially after having taken your eyes off the project for weeks, months, or years. If a developer forgets to inherit, it isn't obvious that there is a security vulnerability in the project.

A more secure solution to this problem is to deny access to all requests, then decorate each action with the roles that are allowed access to the actions ("whitelisting"; preventing access to all users unless manually allowed). Now if a developer forgets to whitelist the proper authorization, the users will let you know and it's as simple as looking at other controllers for a reminder about how to give proper access. However, at least there is no major security vulnerability.

In App_Start/FilterConfig.cs file, modify the FilterConfig class:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        ...

        //Deny access to all controllers and actions so that only logged in Administrators can access them by default
        filters.Add(new System.Web.Mvc.AuthorizeAttribute() { Roles = "Administrator" });
    }

This makes all actions inaccessible unless the user is logged in as an Administrator. Then for each action that you want a different authorized user to have access to, you simply decorate it with [OverrideAuthorization] and [Authorize].

In your business logic, this allows you to use the Authorize attribute in a variety of ways without ever needing to worry about unauthorized users from accessing any functionality. Below are some examples.

Example 1 - Only logged in Administrator and Dispatcher users will be allowed to access Index() Get and Post methods.

public class MarkupCalculatorController : Controller //Just continue using the default Controller class.
{
    // GET: MarkupCalculator
    [OverrideAuthorization]
    [Authorize(Roles = "Administrator,Dispatcher")]
    public ActionResult Index()
    {
        //Business logic here.

        return View(...);
    }

    // POST: DeliveryFeeCalculator
    [HttpPost]
    [ValidateAntiForgeryToken]
    [OverrideAuthorization]
    [Authorize(Roles = "Administrator,Dispatcher")]
    public ActionResult Index([Bind(Include = "Price,MarkedupPrice")] MarkupCalculatorVM markupCalculatorVM)
    {
        //Business logic here.

        return View(...);
    }
}

Example 2 - Only authenticated users will be allowed to access the Home controller's Index() method.

public class HomeController : Controller
{
    [OverrideAuthorization]
    [Authorize] //Allow all authorized (logged in) users to use this action
    public ActionResult Index()
    {
        return View();
    }

}

Example 3 - Unauthenticated users (i.e. anonymous users) can be allowed to access methods by using the [AllowAnonymous] attribute. This also automatically overrides the global filter without needing the [OverrideAuthorization] attribute.

    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl)
    {
        ViewBag.ReturnUrl = returnUrl;
        return View();
    }

    //
    // POST: /Account/Login
    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        ...
    }

Example 4 - Only admins will be allowed access to methods that lack the [Authorize] attribute.

public class LocationsController : Controller
{

    // GET: Locations
    public ActionResult Index()
    {
        //Business logic here.
        return View(...);
    }
}

Some notes.

You must use the [OverrideAuthorization] attribute if you want to limit the access to a particular action to specific roles. Otherwise, the [Authorize] attribute properties will be ignored and only the default role (Administrator in my example) will be allowed, even if you specify other roles (e.g. Dispatcher, etc.) because of the global filter. Any unauthorized users will be redirected to the login screen.

Using the [OverrideAuthorization] attribute causes the action to ignore the global filter you set. Therefore, you must reapply the [Authorize] attribute whenever you use the override so that the action remains secure.

Regarding whole areas and controllers

To restrict by areas, as you are asking, put the [OverrideAuthorization] and [Authorize] attributes on the controller instead of the individual actions.

Share:
38,613
TomG
Author by

TomG

Updated on December 17, 2020

Comments

  • TomG
    TomG over 3 years

    I've an Admin area and I want only Admins to enter the area. I considered adding the Authorized attribute to every controller in the Admin area. Isn't there an elegant solution or is this feature not there in the framework itself?

    EDIT: I'm sorry, I should to have mentioned this before. I'm using a custom AuthorizedAttribute derived from AuthorizeAttribute.

  • Cédric Rup
    Cédric Rup about 14 years
    Well, is the a good way to be sure a controller is hit only via a single URL ? By a correct planing of routes maybe ?
  • Levi
    Levi about 14 years
    There's no way to ensure a controller is accessible only via a single URL. Routes are simply a mechanism for accessing controllers; they are not the mechanism. This is why any security attributes need to be applied directly to the controllers themselves, not to the routes (and by extension, the areas). For example, consider the introduction of MvcHandler.ashx in MVC 3. This would call into the MVC framework directly by bypassing all of Routing.
  • Cédric Rup
    Cédric Rup about 14 years
    Ok... does it mean there's no correct answer to the question beside a correct use of security attributes ?
  • Levi
    Levi about 14 years
    Correct. Always assume that every public method of every controller is web-callable by the world. Then it makes sense that [Authorize], [NonAction], and other attributes are the correct way to secure these resources.
  • RickAndMSFT
    RickAndMSFT about 12 years
    See my blog post Securing your ASP.NET MVC 4 App and the new AllowAnonymous Attribute
  • jenson-button-event
    jenson-button-event over 11 years
    SURELY the more sensible way is to provide a mechanism that FORCES the filter upon all controllers on a given area irrespective of your argument. a developer is more likely to forget to add the attribute. its like systems guys suggesting their users secure each file in a folder individually because windows prevents the sys guy doing it at the folder level. another half-baked idea IMO. keep it DRY for goodness sake!
  • Piotr Kula
    Piotr Kula about 11 years
    That works well for a single controller. But how do we do it for an entire area?
  • Piotr Kula
    Piotr Kula about 11 years
    Yea that does sound like a good idea. Besides MSDN suggests similar solutions for other problems. Inheritance is good. I like this answer.
  • Gudradain
    Gudradain over 9 years
    I really don't get why you would subclass every controller in admin section instead of simply writing your 1 line attribute above the class definition.
  • Quango
    Quango over 9 years
    The simple answer is DRY - en.wikipedia.org/wiki/Don't_repeat_yourself - i can change which roles are protected in one line of code, instead of hunting for each [Authorize] attribute
  • Abhimanyu
    Abhimanyu over 8 years
    This looks very clean to me. But what if any developer next to me forgets to inherit AdminController in his new controller inside area? Next thing, when I add this in my implementation (identityserver3) and user directly hits area controller I see things looping on identity server login page all the time.
  • Quango
    Quango over 8 years
    @AbhimanyuKumarVatsa developer forgetting to inherit from the base controller is the same problem as a developer forgetting to add an [Authorize] attribute to the controller. The solution is not a fault here.
  • aiwyn
    aiwyn over 6 years
    Can this please not be the answer? It is very misleading and introduces a grave security vulnerability. Please see my answer.
  • niico
    niico over 6 years
    Hi from the future - Attribute Routing is a MUCH better way of controlling your routes in a granular - and much easier to understand - way, with way less chance of random multiple routes to the same pages. Highly recommended.
  • Mikael Dúi Bolinder
    Mikael Dúi Bolinder over 5 years
    @ppumkin, Inherited is set to true for AuthorizeAttribute so just inherit it. msdn.microsoft.com/en-us/library/…
  • مهدی
    مهدی almost 2 years
    how to set for Areas/Identity/Pages/Account/Manage folder? options.Conventions.AuthorizeAreaFolder("Identity", "/Manage","AdminPolicy"); not worked