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

6

87

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.

Big answered 24/2, 2016 at 18:1 Comment(0)
F
71

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.

Foreordain answered 24/2, 2016 at 18:28 Comment(6)
If policies are cumulative, why are defaults replaced when using custom ones? The heart of this question is coming from this question. I'm declaring custom policies and don't want unauthenticated requests ever getting into my authorization handlers. The current way I'm using is from step 2 in your authorization workshop (authorizing all endpoints and putting [AllowAnonymous] where needed). Feels like an antipattern, but I could be stupid!Pameliapamelina
Basically we assume if you're setting your own policies you know what you're doing. Applying a policy indicates you are going to override the default.Foreordain
Understood. Just feels like default policy should be a "baseline" as if it's your first policy in a collection of custom ones.Pameliapamelina
Yea, it's not so much a default as "Do this if there's nothing has been specified."Foreordain
@steamrolla, they are cumulative but asp net authorization uses the Lest privilege approach to handle the security, all of them must pass, in your case, [AllowAnonymous] passed but might be blocked by following policies.Debbradebby
How to create a policy with an AND condition?Breach
R
41

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://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#using-a-func-to-fulfill-a-policy

Retroversion answered 18/7, 2018 at 16:13 Comment(0)
E
7

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://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1#why-would-i-want-multiple-handlers-for-a-requirement

Empirical answered 30/10, 2018 at 14:29 Comment(1)
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));Debra
P
5

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()));
            });
Pudency answered 5/4, 2021 at 2:40 Comment(0)
H
4

I use Policy and Role:

[Authorize(Policy = "ManagerRights", Roles = "Administrator")]
Hy answered 25/3, 2020 at 13:7 Comment(2)
would be ok but need premium subscription for custom roles if you need other than built in rolesGrooms
@MarkHomer IIRC, On Windows with ASP.NET Core, Roles are also claims, and this should work. Do you mean by 'premium subscription' having an AD Domain available?Koester
E
2

Here is a solution using a custom authorization attribute, IMO it is by far the cleanest way to do this.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MultiPolicyAuthorizeAttribute : Attribute, IAsyncAuthorizationFilter
{
    private readonly string[] _policies;

    public MultiPolicyAuthorizeAttribute(params string[] policies)
    {
        _policies = policies;
    }

    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var authService = context.HttpContext.RequestServices.GetService<IAuthorizationService>();

        if (authService == null)
        {
            context.Result = new ForbidResult();
            return;
        }

        bool isAuthorized = false;
        foreach (var policy in _policies)
        {
            var authorized = await authService.AuthorizeAsync(context.HttpContext.User, policy);
            if (authorized.Succeeded)
            {
                isAuthorized = true;
                break;
            }
        }

        if (!isAuthorized)
        {
            context.Result = new ForbidResult();
        }
    }
}

Note that it's not hard to extend this attribute to perform arbitrary logic based on the specified policies or roles.

Edible answered 16/1, 2024 at 20:14 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.