ASP.NET Core Authorization: Combining OR requirements
Asked Answered
S

3

17

I'm not sure how to implement combined "OR" requirements in ASP.NET Core Authorization. In previous versions of ASP.NET this would have been done with roles, but I'm trying to do this with claims, partly to understand it better.

Users have an enum called AccountType that will provide different levels of access to controllers/actions/etc. There are three levels of types, call them User, BiggerUser, and BiggestUser. So BiggestUser has access to everything the account types below them have and so on. I want to implement this via the Authorize tag using Policies.

So first I have a requirement:

public class TypeRequirement : IAuthorizationRequirement
{
    public TypeRequirement(AccountTypes account)
    {
        Account = account;
    }

    public AccountTypes Account { get; }
}

I create the policy:

services.AddAuthorization(options =>
{
    options.AddPolicy("UserRights", policy => 
        policy.AddRequirements(new TypeRequirement(AccountTypes.User));
});

The generalized handler:

public class TypeHandler : AuthorizationHandler<TypeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TypeRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == "AccountTypes"))
        { 
            context.Fail();
        }

        string claimValue = context.User.FindFirst(c => c.Type == "AccountTypes").Value;
        AccountTypes claimAsType = (AccountTypes)Enum.Parse(typeof(AccountTypes), claimValue);
        if (claimAsType == requirement.Account)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

What I would to do is add multiple requirements to the policy whereby any of them could satisfy it. But my current understanding is if I do something like:

options.AddPolicy("UserRights", policy => policy.AddRequirements(
    new TypeRequirement(AccountTypes.User),
    new TypeRequirement(AccountTypes.BiggerUser)
);

Both requirements would have to be satisfied. My handler would work if there was someway in AddRequirements to specify an OR condition. So am I on the right track or is there a different way to implement this that makes more sense?

Splay answered 20/3, 2018 at 16:52 Comment(2)
Imagine you have 2 policies strongA and strongB. They require respectively claimA and claimB. You want a policy that is fullfilled for the users already ok for strongA OR strongB . Then, the solution that comes to my mind is : - 1) Have a new claim claimC - 2) change strongA and strongB so that they both require claimC as well. - 3) grant claimC to all users that already have claimA or claimB (now you are back to you original setup). and lastly . 4) create new policy lightC , requiring only claimC -> now all users that fullfill policy strongA OR strongB will also fullfill policy lightCVino
To keep this project moving I'm doing it all in one handler for now, it's just a very inelegant bunch of if(this requirement){check for these type} repeated once for each account type.Splay
D
14

The official documentation has a dedicated section when you want to implement an OR logic. The solution they provide is to register several authorization handlers against one requirement. In this case, all the handlers are run and the requirement is deemed satisfied if at least one of the handlers succeeds.

I don't think that solution applies to your problem, though; I can see two ways of implementing this nicely


Provide multiple AccountTypes in TypeRequirement

The requirement would then hold all the values that would satisfy the requirement.

public class TypeRequirement : IAuthorizationRequirement
{
    public TypeRequirement(params AccountTypes[] accounts)
    {
        Accounts = accounts;
    }

    public AccountTypes[] Accounts { get; }
}

The handler then verifies if the current user matches one of the defined account types

public class TypeHandler : AuthorizationHandler<TypeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TypeRequirement requirement)
    {

        if (!context.User.HasClaim(c => c.Type == "AccountTypes"))
        { 
            context.Fail();
            return Task.CompletedTask;
        }

        string claimValue = context.User.FindFirst(c => c.Type == "AccountTypes").Value;
        AccountTypes claimAsType = (AccountTypes)Enum.Parse(typeof(AccountTypes),claimValue);
        if (requirement.Accounts.Any(x => x == claimAsType))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}

This allows you to create several policies that will use the same requirement, except you get to define the valid values of AccountTypes for each of them

options.AddPolicy(
    "UserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.User, AccountTypes.BiggerUser, AccountTypes.BiggestUser)));

options.AddPolicy(
    "BiggerUserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.BiggerUser, AccountTypes.BiggestUser)));

options.AddPolicy(
    "BiggestUserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.BiggestUser)));

Use the enum comparison feature

As you said in your question, there's a hierarchy in the way you treat the different values of AccountTypes:

  • User has access to some things;
  • BiggerUser has access to everything User has access to, plus some other things;
  • BiggestUser has access to everything

The idea is then that the requirement would define the lowest value of AccountTypes necessary to be satisfied, and the handler would then compare it with the user's account type.

Enums can be compared with both the <= and >= operators, and also using the CompareTo method. I couldn't quickly find robust documentation on this, but this code sample on learn.microsoft.com shows the usage of the lower-than-or-equal operator.

To take advantage of this feature, the enum values need to match the hierarchy you expect, like:

public enum AccountTypes
{
    User = 1,
    BiggerUser = 2,
    BiggestUser = 3
}

or

public enum AccountTypes
{
    User = 1,
    BiggerUser, // Automatiaclly set to 2 (value of previous one + 1)
    BiggestUser // Automatically set to 3
}

The code of the requirement, the handler and the declaration of the policies would then look like:

public class TypeHandler : AuthorizationHandler<TypeRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TypeRequirement requirement)
    {

        if (!context.User.HasClaim(c => c.Type == "AccountTypes"))
        { 
            context.Fail();
            return Task.CompletedTask;
        }

        string claimValue = context.User.FindFirst(c => c.Type == "AccountTypes").Value;
        AccountTypes claimAsType = (AccountTypes)Enum.Parse(typeof(AccountTypes),claimValue);
        if (claimAsType >= requirement.MinimumAccount)
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
options.AddPolicy(
    "UserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.User)));

options.AddPolicy(
    "BiggerUserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.BiggerUser)));

options.AddPolicy(
    "BiggestUserRights",
    policy => policy.AddRequirements(new TypeRequirement(AccountTypes.BiggestUser)));
Distrust answered 21/3, 2018 at 21:13 Comment(3)
Great answer but I think you meant options.AddPolicy("HostRights", policy => policy.AddRequirements(new AccountTypeRequirement(new AccountTypes []{ AccountTypes.Host, AccountTypes.Admin, AccountTypes.SuperAdmin }))); and so on?Splay
@Splay Both syntaxes are correct in this case; the one I used is allowed because of the use of the params keyword in the parameter declaration (params AccountType[] accounts). You can read more about it on this official documentation page.Parturifacient
I missed the params keyword. Either way I used the enum implementation and in the end only had to change a few lines of codeSplay
B
4

Copied from my original answer for those looking for short answer (note: below solution does not address hierarchy issues).

You can add OR condition in Startup.cs:

Ex. I wanted only "John Doe", "Jane Doe" users to view "Ending Contracts" screen OR anyone only from "MIS" department also to be able to access the same screen. The below worked for me, where I have claim types "department" and "UserName":

services.AddAuthorization(options => {
    options.AddPolicy("EndingContracts", policy =>
        policy.RequireAssertion(context => context.User.HasClaim(c => (c.Type == "department" && c.Value == "MIS" ||
        c.Type == "UserName" && "John Doe, Jane Doe".Contains(c.Value)))));
});
Barytes answered 12/7, 2018 at 15:14 Comment(0)
A
0

This is an old question, and there are great answers.

However, using claims to achieve what in essence is inheritance based authorization should be done with distinct claims for each tier, i.e.

  • Regular user should have claimA
  • Manager should have claimB and claimA to be able to do everything that a regular user can do + manager's own privileges
  • Admin should have claimC and claimB and claimA to be able to do what only admin can do + everything manager and regular user can do

The separation of levels is clear and distinct, easily extensible, and flexible.

Trying to do the tiered/inheritance approach leads to big problems in the future in the same way as inheritance often does in code. What if there is a new requirement tomorrow that admin and user should be able to do or see something in the system that a manager cannot do?

With claims, you create claimD that you give to admins and users, but not to managers. With inheritance based solutions, it's much harder to achieve.

Atc answered 27/11, 2023 at 4:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.