简体   繁体   中英

Clarifying Identity Authorization: using Claims as Roles, Roles and Claims or Role Claims

I'm starting with ASP.NET Identity's Claim authorization and I would like to clarify the way of proceeding with them if I need the "roles" concept in my app.

Note: I'm really new with this, so all the concepts are flying in my head, please be kind, and further clarifications/Corrections about any concept will be much appreciated.

1.- Suppose, I need the "roles" concept for Admin and User roles, so my first though was to add claims to ApplicationUser s like:

user.Claims.Add(new IdentityUserClaim<string> { ClaimType = "Role", ClaimValue = "Admin" });

*Where "user" is an ApplicationUser .

But then I read that it is already done by the framework as it has some predefined claim types, so the code above could be:

user.Claims.Add(new IdentityUserClaim<string> { ClaimType = ClaimTypes.Role, ClaimValue = "Admin" });

Is that approach correct? Or should i use the "old" role concept and add a role to the user like:

await _roleManager.CreateAsync(new IdentityRole("Admin"));    
await _userManager.AddToRoleAsync(user, "Admin");

2.- Now suppose that I have roles defined as claims, how could I check the authotization of them? I mean, will it work?

[Authorize(Roles = "Admin")]

Or should I include a Policy statement to check the role claim?

/* In startup ConfigureServices method*/
options.AddPolicy("IsAdmin", policy => {
                policy.RequireClaim(ClaimTypes.Role, "Admin");
                });

...

/*In a controller class*/
[Authorize(Policy = "IsAdmin")]
<controller here>

3.- And now, what is the correct way of storing my custom claims? I mean, ASP.NET's ClaimTypes class is just a bunch of const string values and all the sample codes about Claims stores them in similar classes like:

public static class ClaimData
{
    public static List<string> AdminClaims { get; set; } = new List<string>
                                                        {
                                                            "Add User",
                                                            "Edit User",
                                                            "Delete User"
                                                        };
}

Is that ok?

Final note.- I've also see at the internet the concept of "Role Claim", which is explained in this blog post: http://benfoster.io/blog/asp-net-identity-role-claims

What is that? If I wasn't confused enough, now there is a third way of Authorizing users. Is it the better way to use roles as claims?

The approach you describe seems correct. Everything depends upon your requirements.

Imagine you have several features in your application, if you choose to use roles, the code belonging to the feature must check everytime if the user is in a particular set of roles to use the feature. This approach becomes quite unmanageable when the features and roles grow, because you must take into account the combination of roles into every single feature. In this example, a user can perform the management operation X only if it is PowerUser or Administrator . Now, this seems easy and strightforward, but, what happens if you add a new role, ALittleBitMorePowerful , which is a User who can also perform the X operation. To achieve this result you have to review everything and change the checks (this implies retesting the whole thing).

If you designed the feature X with a claim CanPerformX , your introduce a layer of abstraction: your code will not care about the role of the user, but will check only for its own claim. If you ever rework how the claims are associated to the users, your effective code will not change (which in the end means no formal regressions have been introduced).

Roles are designed to be broad while Claims have been designed to be fine grain. However, as you read in the link, you may think a role as "big claim", or a claim as a "small role".

I post a small excerpt of a code of mine which supports custom roles but fixed claims. Define the claims

    internal static class PolicyClaims
    {
        public const string AdministratorClaim = @"http://myorganization/2019/administrator";
        public const string Operation1Claim = @"http://myorganization/2019/op1";
        public const string Operation2Claim = @"http://myorganization/2019/op2";
        public const string ObtainedClaim = @"true";
    }

Define the policies

    internal static class Policies
    {
        public const string RequireAdministrator = "RequireAdministrator";
        public const string RequireOp1 = "RequireOp1";
        public const string RequireOp2 = "RequireOp2";

        public const string AlwaysDeny = "AlwaysDeny";

        public static void ConfigurePolicies(IServiceCollection services)
        {
            services.AddAuthorization(options => options.AddPolicy(RequireAdministrator, policy => policy.RequireClaim(PolicyClaims.AdministratorClaim)));
            services.AddAuthorization(options => options.AddPolicy(RequireOp1, policy => policy.RequireClaim(PolicyClaims.Operation1Claim)));
            services.AddAuthorization(options => options.AddPolicy(RequireOp2, policy => policy.RequireClaim(PolicyClaims.Operation2Claim)));
            services.AddAuthorization(options => options.AddPolicy(AlwaysDeny, policy => policy.RequireUserName("THIS$USER\n\r\t\0cannot be created")));
        }
    }

Register the policies in Startup.RegisterServices

    Policies.ConfigurePolicies(services);

Where you authenticate the user, decide which claims you need to add based upon your logic (omitted some parts to focus on the concepts)

    [AllowAnonymous]
    [Route("api/authentication/authenticate")]
    [HttpPost()]
    public async Task<IActionResult> Authenticate([FromBody] LoginModel model)
    {
        if (ModelState.IsValid)
        {
            var user = m_UserManager.Users.FirstOrDefault(x => x.UserName == model.UserName);

            if (user == null)
            {
                ...
            }
            else
            {
                var result = await m_SignInManager.CheckPasswordSignInAsync(user, model.Password, false);
                if (result.Succeeded)
                {
                    var handler = new JwtSecurityTokenHandler();
                    var tokenDescriptor = new SecurityTokenDescriptor
                    {
                        Subject = new ClaimsIdentity(new Claim[]
                        {
                        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(ClaimTypes.Name, model.UserName)
                        }),
                        Expires = DateTime.UtcNow.AddHours(2),
                        SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(InstanceSettings.JWTKey), SecurityAlgorithms.HmacSha256Signature)
                    };

                    var roles = await m_UserManager.GetRolesAsync(user);

                    AddClaims(tokenDescriptor, roles);

                    var token = handler.CreateToken(tokenDescriptor);
                    var tokenString = handler.WriteToken(token);

                    return ...
                }
                else
                {
                    ...
                }
            }
        }
        return ...
    }

    private static void AddClaims(SecurityTokenDescriptor tokenDescriptor, IList<string> roles)
    {
        if (roles.Any(x => string.Equals(Constants.AdministratorRoleName, x, StringComparison.OrdinalIgnoreCase)))
        {
            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.AdministratorClaim, PolicyClaims.ObtainedClaim));

            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation1Claim, PolicyClaims.ObtainedClaim));
            tokenDescriptor.Subject.AddClaim(new Claim(PolicyClaims.Operation2Claim, PolicyClaims.ObtainedClaim));
        }
        ... query the database and add each claim with value PolicyClaims.ObtainedClaim ...
    }

Finally, you can use the policies to protect your code:

    [Authorize(Policy = Policies.RequireAdministrator)]
    [HttpPost("execute")]
    public async Task<IActionResult> ExecuteOperation([FromBody] CommandModel model)
    {
        ...
    }

Note that in this approach, I hardcoded certain claims to the administrator because I'd like to prevent the administrator removing certain claims. However, this is not mandatory.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM