Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core Authorization: Combining OR requirements

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?

like image 985
greedyLump Avatar asked Mar 20 '18 16:03

greedyLump


2 Answers

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 docs.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)));
like image 136
Mickaël Derriey Avatar answered Sep 29 '22 22:09

Mickaël Derriey


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)))));
});
like image 40
Richard Mneyan Avatar answered Sep 29 '22 22:09

Richard Mneyan