10

I have defined 2 policies, ADD and SUB as shown below.

options.AddPolicy("ADD", policy =>
    policy.RequireClaim("Addition", "add"));

options.AddPolicy("SUB", policy =>
    policy.RequireClaim("Substraction", "subs"));

All what I want to do isto include 2 policies on a controller method. How can I perform this operation.

 [Authorize(Policy = "ADD, SUB")]
 [HttpPost]
 public IActionResult PerformCalculation()
 {
 }

However, this gives me an error:

InvalidOperationException: The AuthorizationPolicy named: 'ADD, SUB' was not found

Sharon Watinsan
  • 9,620
  • 31
  • 96
  • 140
  • 1
    Possible duplicate of [ASP.NET 5 Authorize against two or more policies](https://stackoverflow.com/questions/35609632/asp-net-5-authorize-against-two-or-more-policies) – HudsonPH Jul 20 '18 at 13:35
  • If you're wanting OR policies it seems you'll have to make a new policy that requires one of the two claims and use that new policy instead – p3tch Jul 20 '18 at 13:49

1 Answers1

17

Update: This solution is now available in the NuGet package TGolla.AspNetCore.Mvc.Filters for which the code can be found published in the GitHub repository TGolla.Swashbuckle.AspNetCore.

The first thing to realize is that the Authorize attribute Policy setting is singular unlike Roles which can be plural and that multiple policies are treated on an AND basis, unlike a list of roles which is treated on an OR basis.

In your example code “ADD, SUB” is considered a single policy name. If you want to attribute you method with both policies, your code should be as follows.

[Authorize(Policy = "ADD")]
[Authorize(Policy = "SUB")]
[HttpPost]
public IActionResult PerformCalculation()
{
}

However, this will not give you the effect you want of either or, since policies are AND together, hence both policies must pass to be authorized. Nor will the suggestions of writing a single policy or a requirements handler to handle the multiple requirements give you the result of treating policies on a OR basis.

Instead, the solution is to create a TypeFilterAttribute that accepts a list of policies and is tied to a IAsyncAuthorizationFilter that test for either or. The following outlines the two classes you will need to define and how to attribute your action method.

The following code defines the new attribute AuthorizeOnAnyOnePolicy class.

/// <summary>
/// Specifies that the class or method that this attribute is applied to requires 
/// authorization based on user passing any one policy in the provided list of policies.
/// </summary>
public class AuthorizeOnAnyOnePolicyAttribute : TypeFilterAttribute
{
    /// <summary>
    /// Initializes a new instance of the AuthorizeOnAnyOnePolicyAttribute class.
    /// </summary>
    /// <param name="policies">A comma delimited list of policies that are allowed to access the resource.</param>
    public AuthorizeOnAnyOnePolicyAttribute(string policies) : base(typeof(AuthorizeOnAnyOnePolicyFilter))
    {
        Regex commaDelimitedWhitespaceCleanup = new Regex("\\s+,\\s+|\\s+,|,\\s+",
            RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.IgnorePatternWhitespace | RegexOptions.Compiled);

        Arguments = new object[] { commaDelimitedWhitespaceCleanup.Replace(policies, ",") };
    }
}

The following code defines the authorization filter class which loops through and executes each policy in the list. Should all the policies fail the result of the authorization context is set to forbid.

public class AuthorizeOnAnyOnePolicyFilter : IAsyncAuthorizationFilter
{
    private readonly IAuthorizationService authorization;
    public string Policies { get; private set; }

    /// <summary>
    /// Initializes a new instance of the AuthorizeOnAnyOnePolicyFilter class.
    /// </summary>
    /// <param name="policies">A comma delimited list of policies that are allowed to access the resource.</param>
    /// <param name="authorization">The AuthorizationFilterContext.</param>
    public AuthorizeOnAnyOnePolicyFilter(string policies, IAuthorizationService authorization)
    {
        Policies = policies;
        this.authorization = authorization;
    }

    /// <summary>
    /// Called early in the filter pipeline to confirm request is authorized.
    /// </summary>
    /// <param name="context">A context for authorization filters i.e. IAuthorizationFilter and IAsyncAuthorizationFilter implementations.</param>
    /// <returns>Sets the context.Result to ForbidResult() if the user fails all of the policies listed.</returns>
    public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
    {
        var policies = Policies.Split(",").ToList();
            
        // Loop through policies.  User need only belong to one policy to be authorized.
        foreach (var policy in policies)
        {
            var authorized = await authorization.AuthorizeAsync(context.HttpContext.User, policy);
            if (authorized.Succeeded)
            {
                return;
            }

        }
            
        context.Result = new ForbidResult();
        return;
    }
}

With the policies defined as shown in the question you would attribute the method as follows.

[AuthorizeOnAnyOnePolicy("ADD,SUB")]
[HttpPost]
public IActionResult PerformCalculation()
{
}

It’s that simple and you will find similar solutions in the following Stack Overflow questions.

Terence Golla
  • 1,018
  • 1
  • 13
  • 12
  • Great explanation. Should be marked as accepted. +1 Upvoted – knoxgon Oct 28 '21 at 10:37
  • I've tried your solution, (thanks by the way for the good explanation), but the context seems empty. There are no claims/user data to compare against. Any idea why? – Medismal Sep 06 '22 at 20:55
  • I'm also getting empty context with no claims. – Rukshán Dikovita Nov 18 '22 at 09:12
  • If the issue "context seems empty" still exist, please go to the recently released GitHub repository https://github.com/tgolla/TGolla.Swashbuckle.AspNetCore and report an issue so I can assist you with resolving it. – Terence Golla Apr 18 '23 at 19:45