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

1

11

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?

Celandine answered 1/9, 2017 at 9:27 Comment(0)
I
2

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.

Ingridingrim answered 6/1, 2019 at 17:54 Comment(4)
How would you reason about not using claims, roles nor policies and simply got the user ID from the token and manually check the privileges against the DB?Rusticate
At the moment I cannot provide any code: you can store the user id in the token (and indeed if you use JWT you already have the appropriatetools to field for that) with the code in the answer. Then you have to disable any token enforcement in the api controller, grab the raw token from the headers (something like var accessToken = Request.Headers[HeaderNames.Authorization];), manually parsethe the token, extract the user ID, perform your lookup and you are done. It is complex, and you are mixing infrastructure code and business code. I wouldn't recommend it. See other comment.Ingridingrim
You could obtain what you ask by using the code posted on the answer and define a policy that instread if requiring a particular claim, simply gets the value of the user ID claim (or the one you want) and then performs the lookup with your criteria. The policy should be applied everywhere. IMHO This approach uses the recommended patterns and allows you to separate the concerns in a cleaner way.Ingridingrim
Ah, I totally agree. I even asked specifically about it. A colleague of mine is heavily convinced that we should do it the custom way and I didn't dare to put my foot down, just in case I'm missing something. But now, I've got in confirmed from three separate source, so I feel more reassured. Feel welcome to pitch in your 2 cents in the linked question if you feel it's beneficial to the readers.Rusticate

© 2022 - 2024 — McMap. All rights reserved.