'ASP.NET 5 Authorize against two or more policies (OR-combined policy)
Is it possible to apply authorization against two or more policies? I am using ASP.NET 5, rc1.
[Authorize(Policy = "Limited,Full")]
public class FooBarController : Controller
{
// This code doesn't work
}
If not, how may I achieve this without using policies? There are two groups of users that may access this controller: "Full" and "Limited". Users may either belong to "Full" or "Limited", or both. They only require to belong to one of the two groups in order to access this controller.
Solution 1:[1]
Not the way you want; policies are designed to be cumulative. For example if you use two separate attributes then they must both pass.
You have to evaluate OR conditions within a single policy. But you don't have to code it as ORs within a single handler. You can have a requirement which has more than one handler. If either of the handlers flag success then the requirement is fulfilled. See Step 6 in my Authorization Workshop.
Solution 2:[2]
Once setting up a new policy "LimitedOrFull" (assuming they match the claim type names) create a requirement like this:
options.AddPolicy("LimitedOrFull", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c =>
(c.Type == "Limited" ||
c.Type == "Full"))));
Solution 3:[3]
Net Core has an option to have multiple AuthorizationHandlers that have the same AuthorizationRequirement type. Only one of these have to succeed to pass authorization https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#why-would-i-want-multiple-handlers-for-a-requirement
Solution 4:[4]
I use Policy and Role:
[Authorize(Policy = "ManagerRights", Roles = "Administrator")]
Solution 5:[5]
Solution with use of dynamically created requirements on demand works best for me:
- Create interfaces of separate "Limited" and "Full" policy requirements:
public interface ILimitedRequirement : IAuthorizationRequirement { }
public interface IFullRequirement : IAuthorizationRequirement { }
- Create custom attribute for authorization:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorizeAnyAttribute : AuthorizeAttribute {
public string[] Policies { get; }
public AuthorizeAnyAttribute(params string[] policies) : base(String.Join("Or", policies))
=> Policies = policies;
}
- Create authorization handlers for
ILimitedRequirement
andIFullRequirement
(Please, note that these handlers process interfaces, not classes):
public class LimitedRequirementHandler : AuthorizationHandler<ILimitedRequirement> {
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ILimitedRequirement requirement) {
if(limited){
context.Succeed(requirement);
}
}
}
public class FullRequirementHandler : AuthorizationHandler<IFullRequirement> {
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IFullRequirement requirement) {
if(full){
context.Succeed(requirement);
}
}
}
- If your authorization handlers are heavy (for example, one of them accesses database) and you don't want one of them to do authorization check if another one has already succeeded or failed, you can use next workaround (remember that order of handler registration directly determines their execution order in request pipeline):
public static class AuthorizationExtensions {
public static bool IsAlreadyDetermined<TRequirement>(this AuthorizationHandlerContext context)
where TRequirement : IAuthorizationRequirement
=> context.HasFailed || context.HasSucceeded
|| !context.PendingRequirements.Any(x => x is TRequirement);
}
public class LimitedRequirementHandler : AuthorizationHandler<ILimitedRequirement> {
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ILimitedRequirement requirement) {
if(context.IsAlreadyDetermined<ILimitedRequirement>())
return;
if(limited){
context.Succeed(requirement);
}
}
}
public class FullRequirementHandler : AuthorizationHandler<IFullRequirement> {
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IFullRequirement requirement) {
if(context.IsAlreadyDetermined<IFullRequirement>())
return;
if(full){
context.Succeed(requirement);
}
}
}
- Register authorization handlers (note that there is no "LimiterOrFullRequirementHandler", these two handlers will deal with combined policy requirement):
//Order of handlers is important - it determines their execution order in request pipeline
services.AddScoped<IAuthorizationHandler, LimitedRequirementHandler>();
services.AddScoped<IAuthorizationHandler, FullRequirementHandler>();
- Now we need to retrieve all
AuthorizeAny
attributes and create requirements for them dynamically using ImpromptuInterface (or any other tool for creating insances of types dynamically):
using ImpromptuInterface;
List<AuthorizeAnyAttribute> attributes = new List<AuthorizeAnyAttribute>();
foreach(Type type in Assembly.GetExecutingAssembly().GetTypes().Where(type => type.IsAssignableTo(typeof(ControllerBase)))) {
attributes.AddRange(Attribute.GetCustomAttributes(type , typeof(AuthorizeAnyAttribute))
.Cast<AuthorizeAnyAttribute>()
.Where(x => x.Policy != null));
foreach(var methodInfo in type.GetMethods()) {
attributes.AddRange(Attribute.GetCustomAttributes(methodInfo , typeof(AuthorizeAnyAttribute))
.Cast<AuthorizeAnyAttribute>()
.Where(x => x.Policy != null));
}
}
//Add base requirement interface from which all requirements will be created on demand
Dictionary<string, Type> baseRequirementTypes = new();
baseRequirementTypes.Add("Limited", typeof(ILimitedRequirement));
baseRequirementTypes.Add("Full", typeof(IFullRequirement));
Dictionary<string, IAuthorizationRequirement> requirements = new();
foreach(var attribute in attributes) {
if(!requirements.ContainsKey(attribute.Policy)) {
Type[] requirementTypes = new Type[attribute.Policies.Length];
for(int i = 0; i < attribute.Policies.Length; i++) {
if(!baseRequirementTypes.TryGetValue(attribute.Policies[i], out Type requirementType))
throw new ArgumentException($"Requirement for {attribute.Policies[i]} policy doesn't exist");
requirementTypes[i] = requirementType;
}
//Creating instance of combined requirement dynamically
IAuthorizationRequirement newRequirement = new { }.ActLike(requirementTypes);
requirements.Add(attribute.Policy, newRequirement);
}
}
- Register all created requirements
services.AddAuthorization(options => {
foreach(KeyValuePair<string, IAuthorizationRequirement> item in requirements) {
options.AddPolicy(item.Key, x => x.AddRequirements(item.Value));
}
}
Solution above allows to handle single requirements same as OR-combined if default AuthorizeAttribute
is handled same as custom AuthorizeAnyAttribute
If solution above is an overkill, manual combined type creation and registration can always be used:
- Create combined "Limited Or Full" policy requirement:
public class LimitedOrFullRequirement : ILimitedRequirement, IFullRequirement { }
- If these two requirements must also be used separately (besides use of combined "Limited Or Full" policy), create interfaces implementations for single requirements:
public class LimitedRequirement : ILimitedRequirement { }
public class FullRequirement : IFullRequirement { }
- Register policies (note that commented out policies are fully optional to register):
services.AddAuthorization(options => {
options.AddPolicy("Limited Or Full",
policy => policy.AddRequirements(new LimitedOrFullRequirement()));
//If these policies also have single use, they need to be registered as well
//options.AddPolicy("Limited",
// policy => policy.AddRequirements(new LimitedRequirement()));
//options.AddPolicy("Full",
// policy => policy.AddRequirements(new FullRequirement()));
});
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Adam Evans |
Solution 2 | |
Solution 3 | |
Solution 4 | Esteban Mellado |
Solution 5 |