ASP.NET 5 Authorize against two or more policies (OR-combined policy)

C#asp.net CoreAuthorizationasp.net Core-Mvc

C# Problem Overview


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.

C# Solutions


Solution 1 - C#

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 - C#

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"))));

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#using-a-func-to-fulfill-a-policy

Solution 3 - C#

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 - C#

I use Policy and Role:

[Authorize(Policy = "ManagerRights", Roles = "Administrator")]

Solution 5 - C#

Solution with use of dynamically created requirements on demand works best for me:

  1. Create interfaces of separate "Limited" and "Full" policy requirements:
    public interface ILimitedRequirement : IAuthorizationRequirement { }
    public interface IFullRequirement : IAuthorizationRequirement { }
  1. 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;
	}
  1. Create authorization handlers for ILimitedRequirement and IFullRequirement (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);
            }
		}
	}
  1. 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);
            }
		}
	}
  1. 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>();
  1. 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);
		}
	}
  1. 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:

  1. Create combined "Limited Or Full" policy requirement:
    public class LimitedOrFullRequirement : ILimitedRequirement, IFullRequirement { }
  1. 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 { }
  1. 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()));
			});

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionpainiyffView Question on Stackoverflow
Solution 1 - C#blowdartView Answer on Stackoverflow
Solution 2 - C#Andrius NaruševičiusView Answer on Stackoverflow
Solution 3 - C#HomulvasView Answer on Stackoverflow
Solution 4 - C#Esteban MelladoView Answer on Stackoverflow
Solution 5 - C#Adam EvansView Answer on Stackoverflow