105

I would like to add Authorization to a controller, for multiple Roles at once.

Normally that would look like this:

[Authorize(Roles = "RoleA,RoleB,RoleC")]
public async Task<ActionResult> Index()
{
}

But I have stored my Roles in consts, since they might change or be extended at some point.

public const RoleA = "RoleA";
public const RoleB = "RoleB";
public const RoleC = "RoleC";

I cannot do this, since the string must be known at compile time:

[Authorize(Roles = string.join(",",RoleA,RoleB,RoleC)]
public async Task<ActionResult> Index()
{
}

Is there a way to circumvent the problem?

I COULD write a const which simply contains "RoleA,RoleB,RoleC" - but I dislike magic strings and this is a magic string. Changing the name of a Role and forgetting to change the combined string would be a disaster.

I am using MVC5. ASP.NET Identity and the Role are known at compile time.

numaroth
  • 1,187
  • 3
  • 22
  • 35
Christian Sauer
  • 8,793
  • 8
  • 43
  • 68
  • are you using public const string RoleA = "RoleA"; or as you have written in question? – Mukesh Modhvadiya Jun 12 '14 at 10:24
  • 1
    possible duplicate of [allow multiple roles to access controller action](http://stackoverflow.com/questions/700166/allow-multiple-roles-to-access-controller-action) – Ryan Kohn Sep 18 '14 at 19:44

5 Answers5

208

Try to create custom authorize attribute like this.

public class AuthorizeRolesAttribute : AuthorizeAttribute
{
    public AuthorizeRolesAttribute(params string[] roles) : base()
    {
        Roles = string.Join(",", roles);
    }
}

Assuming your roles will be the same for multiple controllers, create a helper class:

public static class Role
{
    public const string Administrator = "Administrator";
    public const string Assistant = "Assistant";
}

Then use it like so:

public class MyController : Controller
{
    [AuthorizeRoles(Role.Administrator, Role.Assistant)]
    public ActionResult AdminOrAssistant()
    {                       
        return View();
    }
}
Tieson T.
  • 20,030
  • 4
  • 69
  • 86
MacGyver
  • 2,780
  • 1
  • 14
  • 14
  • 13
    Now that's an idea worthy of Mac Gyver ;) – Christian Sauer Jun 12 '14 at 10:47
  • 1
    I also like this solution a lot, especially because I can let my Role be an enum rather than a string. What would a good namespace and location in the project hierarchy be for placing this custom authorize attribute? – Simon Shine Oct 05 '15 at 09:46
  • @SimonShine - I believe that, you should place it in web project e.g. in Attributes folder. However if you have multiple projects in one solution you'll need to move this to another library visible for every project. – MacGyver Oct 05 '15 at 09:48
  • @MacGyver: An Attributes folder seems like the logical choice, although I don't know if I'll be adding enough attributes to justify creating a top-level directory yet. What to name such directory is also discussed in http://stackoverflow.com/questions/7163698/mvc3-where-to-place-custom-attribute-classes – Simon Shine Oct 05 '15 at 09:58
  • @SimonShine Hey, nothing is stopping you to place this class wherever you want. Unless it's not used in views - there is always a refactoring. – MacGyver Oct 05 '15 at 10:24
  • 4
    I am not sure what is going on here, but this did NOT help me, any user regardless of the role was able to access the method. – Urielzen Mar 07 '16 at 07:08
  • Very Nice SIR, Works like a charm – Chef_Code Mar 15 '16 at 10:08
  • 2
    Same issue as @Urielzen, but it was fixed by the answer below from Jerry Finegan (using "System.Web.Mvc.AuthorizeAttribute and NOT System.Web.Http.AuthorizeAttribute") – RJB Sep 16 '19 at 18:20
16

The best and simplest way I found to resolve this problem is just to concatenate roles in the Authorize attribute.

[Authorize(Roles = CustomRoles.Admin + "," + CustomRoles.OtherRole)]

with CustomRole a class with constant strings like this :

public static class CustomRoles
{
    public const string Admin = "Admin";
    // and so on..
}
Eric Eskildsen
  • 3,137
  • 1
  • 28
  • 43
ChristopheHvd
  • 213
  • 3
  • 9
  • 2
    Valueable; but this should be a comment; not an answer. – GhostCat Mar 01 '17 at 15:54
  • Both your answer and the accepted answer will trigger authorization if implemented correctly (I'm using the accepted in a production web app). Proposing an edit to remove the comments about the accepted answer. – Eric Eskildsen Oct 31 '18 at 18:29
  • If the Roles were Enums then you can use something like, [Authorize(Roles = nameof(UserRoleEnum.User) + "," + nameof(UserRoleEnum.Admin))] – Varun Sep 27 '20 at 12:13
13

Make sure you are deriving your custom attribute class off System.Web.Mvc.AuthorizeAttribute and NOT System.Web.Http.AuthorizeAttribute.

I ran into the same problem. Once I changed it, everything worked.

You may also want to add the following to your custom attribute class:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)] 
Sanket
  • 17,295
  • 8
  • 63
  • 76
Jerry Finegan
  • 131
  • 1
  • 4
  • I just tried this and found referencing the library `System.Web.Http.AuthorizeAttribute` INSTEAD OF `System.Web.Mvc.AuthorizeAttribute` – fraser jordan Apr 27 '18 at 03:56
4

What i did is the answer in @Tieson

I tweak a little in his answer. Instead of string.Join why not convert it to list?

Here is my answer:

public class AuthorizeRolesAttribute : AuthorizeAttribute
{
    private new List<string> Roles;
    public AuthorizeRolesAttribute(params string[] roles) : base()
    {
        Roles = roles.toList()
    }
}

And then check the if the role is valid overriding OnAuthorization

public override void OnAuthorization(HttpActionContext actionContext)
{
            if (Roles == null)
                HandleUnauthorizedRequest(actionContext);
            else
            {
                ClaimsIdentity claimsIdentity = HttpContext.Current.User.Identity as ClaimsIdentity;
                string _role = claimsIdentity.FindFirst(ClaimTypes.Role).Value;
                bool isAuthorize = Roles.Any(role => role == _role);

                if(!isAuthorize)
                    HandleUnauthorizedRequest(actionContext);
            }
        }

And there you have it, it is now validating if the role is authorized to access the resource

1

I feel like a custom authorize attribute is overkill for this issue unless you have a large amount of roles.

Since the string must be known at compile time, why not make a static Role class that contains public strings of the roles you have defined, and then add comma separated strings with certain roles that you want to authorize:

public static class Roles
{
    public const string ADMIN = "Admin";
    public const string VIEWER = "Viewer";

    public const string ADMIN_OR_VIEWER = ADMIN + "," + VIEWER;
}

And then you can use the Authorize Attribute like so on the Controller Class or the Controller Method (or both):

[Authorize(Roles = Roles.ADMIN]
public class ExampleController : Controller
{
    [Authorize(Roles = Roles.ADMIN_OR_VIEWER)
    public ActionResult Create()
    {
        ..code here...
    }
}
  • 1
    This example doesn't work, or at least not the way you might think. For example, while novel the `ADMIN_OR_VIEWER` role on the action is redundant because you will not be allowed to get to the `Create` method if you don't already have the `ADMIN` role. In this case `VIEWER` will never be able to invoke `Create` method. – John Leidegren Oct 05 '17 at 08:50
  • This solution is not scalable too. There will be a point where you have too many roles with different actions and you shouldnt create every combination – EduLopez Dec 24 '17 at 16:27