Search code examples
c#asp.net-coreasp.net-identityasp.net-core-2.0claims-based-identity

best practice: customize asp.net core identity authorization


I am fairly new with asp.net identity server. I know how to customize the IdentityUser entity and scaffold / override Identity UI. I'm looking for a high level how-to, best practice on authorization given these circumstances:

A component within the application needs to consider these items for authorization:

  • roles
  • location
  • division
  • entity (the specific component within the application. Like "Manage News")

Coming from a class web forms background, I tend to think like so to define roles:

public int assignId; //key
public int userId;
public int roleId;
public int? locationId;
public int? divisionId;
public int? entityId;

Real World Scenarios

  • User "Adam" with Role "Global Admin" has rights to everything.
  • User "Joe" with Role "Admin" and Location "Indy" has rights to every Entity in the "Indy" location.
  • User "Blow" with Role "Admin" and Location "Indy" and Division "IT" has rights to every "IT" entity in "Indy"
  • User "Joe" with Role "Admin" and Location "Chicago" and Division "Safety" and Entity "News" could post news for Safety division in Chicago only. (In addition to the rule above regarding Joe and Indy)

So, my question?

What would be the best way to handle these sort of authorization rules / policies using asp.net core 2.1 identity? Adding claims like "loc"/"indy"?

Would I setup custom authorization handlers to check claims? Or would I set up a middleware table to handle the associations like I would back in the traditional webforms days? Or is there a best practice for this scenario??

Thank you for your help!!!!!!!


Solution

  • Earlier verions of ASP.NET used a Role-Based approach . However , in this case the new Claims-Based approach is prefered . Because we can hardly determine which role is allowed to access the resource .

    Let's say the three users and claims you described above :

    • Adam : Role= Global Admin
    • Joe : Role=Admin; Location=Indy&Location=Chicago ; Division=Safety; Entity=News
    • Blow : Admin ; Location = Indy ; Division = IT ;

    the AspNetUserClaims table records the claims of users as bleow :

    ID | UserID | ClaimType | ClaimValue |

    ----|:------|:----------|:----------:|

    3 | 3ff3d2db-5a8f-4b01-99b5-fe46d22c240c | Role | Global Admin

    4 | cea5d395-fd46-4e6a-aa81-2f4c011b74be | Role | Admin

    5 | cea5d395-fd46-4e6a-aa81-2f4c011b74be | Location | Indy

    6 | cea5d395-fd46-4e6a-aa81-2f4c011b74be | Location | Chicago

    8 | cea5d395-fd46-4e6a-aa81-2f4c011b74be | Division | Safety

    9 | cea5d395-fd46-4e6a-aa81-2f4c011b74be | Entity | News

    10 | b60c7b75-e31b-4856-ba98-666d013c8201 | Role | Admin

    11 | b60c7b75-e31b-4856-ba98-666d013c8201 | Location | Indy

    15 | b60c7b75-e31b-4856-ba98-666d013c8201 | Division | IT

    As you see , the records of Claim-Based approach is rather simple and clean .When there's a need to authorize user , we can compare the user's claims with a policy :

    services.AddAuthorization(opts=> {
        // ... other policy ...
        // ...
        opts.AddPolicy("Check:Role|Location|Division|Entity", pb=>
            pb.RequireAssertion(async context=> await RldeChecker.Handle(context) )
        );
    })
    

    the Checker.Handle(context) here is a simple static method which receives an instance of AuthorizationHandlerContex as parameter and check if a user can access some specific resource .

    To make it more clear , we can add a PolicyChecker/ folder , and place the RldeChecker class into it :

    public class RldeChecker
    {
        // ...
    
        public static async Task<bool> Handle(AuthorizationHandlerContext context) {
    
            var user = context.User;
            // bypass all checks
            if (user.HasClaim("Role","Global Admin" )) { return true; }
            try
            {
                // retrieve the user claims
                var userLocation = user.FindFirst("Location")?.Value;
                var userDivision = user.FindFirst("Division")?.Value;
                var userEntity = user.FindFirst("Entity")?.Value;
                // retrieve the resource that the user want to access at runtime
                var resource = (Dictionary<string, string>)context.Resource; 
                var targetLocation = resource["Location"];
                var targetDivision = resource["Division"];
                var targetEntity = resource["Entity"];
    
                // check for local admin
                // ...
            }
            catch {
                return false;
            }
            return false;
        }
    }
    

    when we want to authorize the user within an action method , we can simply inject an instance of IAuthorizationService to check by authService.Authorize(user,resource,"Check:Role|Location|Division|Entity"). In addition , the claim-Based approach allow us to use it in a service way and then we can inject it everywhere as we need , for example , displaying different contents according to current user's Location/Division/Entity :

    var resource = new Dictionary<string, string>() {
        { "Location","Indy"},
        { "Division","IT"},
        { "Entity","News"},
    };
    var x = await this._authorizationService.AuthorizeAsync(User, resource, "Check:Role|Location|Division|Entity");
    if (x.Succeeded)
    {
        return View();
    }
    else
    {
        return new ForbidResult();
    }