Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 ApplicationUsers 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?

like image 787
MorgoZ Avatar asked Sep 01 '17 09:09

MorgoZ


People also ask

How does role based authorization work?

Role-based authorization checks specify which roles which the current user must be a member of to access the requested resource. The controller SalaryController is only accessible by users who are members of the HRManager role or the Finance role.

How do you use identity claims?

Identity Claims One particular right is that of identity. Claims that possess this right make a statement about the identity of the entity. For example, a claim of type "user principal name" (UPN) with a value of "[email protected]" and a right of Identity indicates a particular identity in a particular domain.

What is claim role?

A Role Claim is a statement about a Role. When a user is a member of a role, they automatically inherit the role's claims. An example of where this feature could be used is for handling application permissions. Roles provide a mechanism to group related users. Permissions determine what members of those roles can do.


1 Answers

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.

like image 140
Yennefer Avatar answered Oct 24 '22 02:10

Yennefer