Blake Rivell
Blake Rivell

Reputation: 13895

Checking for one of multiple policies with Authorize attribute in ASP.NET Core Identity

I have setup a standard authentication system up in an ASP.NET Core application.

Users, Roles, RoleClaims(acting as permissions)

In Startup.cs I create a policy for each Role and each Permission. Assuming this would give me full flexibility in my Views to be able to say I want this button to show if user is part of a role that has claim DeleteCustomer or if User belongs to role Superuser.

How can I do an OR condition using the Authorize attribute. For example all throughout my site I want SuperuserRole Policy to have full permission to everything.

Over an action method let's say I have the following:

[Authorize(Policy = "EditCustomer")]

This will require that the logged in user is assigned to a role that has the claim: Edit.Customer since I am creating a policy for the claim Edit.Customer. This is all working fine, but how do I say I would like any User with the Role Superuser to be able to access EditCustomer action method. Superuser is in the database as a Role and additionally added as a policy called RequireSuperUser.

Upvotes: 8

Views: 14044

Answers (3)

Riza
Riza

Reputation: 1184

We can inject DBContext (if using EF, or user repository if not) or HttpContextAccessor to the AuthorizationHandler to achieve this.

By injecting EF Context (or user repository), we could query database to figure out if the current user is a superuser.

Or by injecting HttpContextAccessor, we can determine if the user is a superuser from that. This approach is faster than the first one but depend on whether User Context have the information needed or not.

To achieve this, assuming what you have are EditCustomerRequirement and EditCustomerAuthorizationHandler:

public class EditCustomerAuthorizationHandler: AuthorizationHandler<EditCustomerRequirement>
{
     readonly AppDbContext _context;
     readonly IHttpContextAccessor _contextAccessor;

     public EditCustomerAuthorizationHandler(DbContext c, IHttpContextAccessor ca)
     {
         _context = c;
         _contextAccessor = ca;
     }

     protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, EditCustomerRequirement requirement)
     {
         //here we have the access to DB and HttpContext
         //we can get the information if the user is superuser from either db or httpContext 
         var isSuperUser = await IsSuperUser(_context);
         //or
         var isSuperUser = IsSuperUser(_contextAccessor);
         if(isSuperUser) 
         {
             context.Succeed(requirement);
         }
         else
         {
            //the current implementation of EditCustomer Policy
         }
     }
}

IsSuperUser is some kind of method that we could make to get the information from either db context or http context.

Upvotes: 0

Shahar Shokrani
Shahar Shokrani

Reputation: 8762

You can use a two different handlers of AuthorizationHandler with the same requirement of IAuthorizationRequirement:

public class FooOrBooRequirement : IAuthorizationRequirement { }

public class FooHandler : AuthorizationHandler<FooOrBooRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationContext context, FooOrBooRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "Foo" && c.Value == true))
        {
            context.Succeed(requirement);
            return Task.FromResult(0);
        }
    }
}

public class BooHandler : AuthorizationHandler<FooOrBooRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationContext context, FooOrBooRequirement requirement)
    {
        if (context.User.HasClaim(c => c.Type == "Boo" && c.Value == true))
        {
            context.Succeed(requirement);
            return Task.FromResult(0);
        }
    }
}

So in the AddPolicy you will have only one requirement that can be fulfilled of by the FooHandler or by the BooHandler:

services.AddAuthorization(authorizationOptions =>
{
    authorizationOptions.AddPolicy(
        "MustBeFooOrBoo",
        policyBuilder =>
        {
            policyBuilder.RequireAuthenticatedUser();
            policyBuilder.AddRequirements(new FooOrBooRequirement());
        });
});

Please note that the AuthorizationHandler provides a more powerful custom policy so it most relevant only for special cases that require a more complex authentication, for example if you want to determine if the user is a resource owner.

For an only simple claims-based policy I'd advice to use the @richard-mneyan's answer.

Upvotes: 2

Richard Mneyan
Richard Mneyan

Reputation: 692

You can add OR condition in Startup.cs:

services.AddAuthorization(options => {
    options.AddPolicy("EditCustomer", policy =>
        policy.RequireAssertion(context => 
        context.User.HasClaim(c => (c.Type == "DeleteCustomer" || c.Type == "Superuser"))));
});

I was facing similar issue where 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)))));
});

Upvotes: 10

Related Questions