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?
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
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)));
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 everythingThe 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)));
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)))));
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With