Users Roles Permissions using ASP.NET Core Identity 3

JReam picture JReam · Feb 7, 2017 · Viewed 10.1k times · Source

I'm stuck on a solution that I would like to provide in an asp.net mvc core application. I would like to provide a solution to the standard User, Roles, Permissions in a web application leveraging the new Claims based approach.

I've been following Ben Foster's logic here (http://benfoster.io/blog/asp-net-identity-role-claims). In the code below (demo quality) I illustrate my methodology which I will comment to help show my quick and dirty test solution.

The challenge I have is, that its not working.

//NOTE: I have found the bug and will comment where I went wrong for future users looking for a similar solution.

Seed Class: This is a quick and dirty solution to seed the database with two new users, two roles and some claims for one of the roles. I did this as its a test app to learn the claims approach to managing authorization for my application. My full solution will provide a way for each Tenant to create their own roles via the UI, associate 1 or many claims to the role(s), then assign a role to a user. I wanted to provide a way for tenants to manage their own users and what they can or cannot do. This is a simple implementation of the claims based approach, as claims have a lot more power than 1:1 relations with policies.

public class DbInitializer
{
    private ApplicationDbContext _context;
    private RoleManager<ApplicationRole> _roleManager;
    private UserManager<ApplicationUser> _userManager;

    public DbInitializer(ApplicationDbContext context,RoleManager<ApplicationRole> roleManager, UserManager<ApplicationUser> userManager)
    {
        _roleManager = roleManager;
        _userManager = userManager;
        _context = context;

    }

    public async Task Initialize()
    {
        //RoleManager<IdentityRole> roleManager = new RoleManager<IdentityRole>();
        //UserManager<ApplicationUser> userManager = new UserManager<ApplicationUser>();

        _context.Database.EnsureCreated();

        // Look for any students.
        if (!_context.Users.Any())
        {
            //create user and admin role

            ApplicationUser adminUser = new ApplicationUser();

            adminUser.Email = "[email protected]";
            adminUser.UserName = "Admin";

            var result = await _userManager.CreateAsync(adminUser, "Password-1");

            var newAdminUser = await _userManager.FindByEmailAsync(adminUser.Email);

            ApplicationRole adminRole = new ApplicationRole();

            adminRole.Name = "Admin";
            adminRole.Description = "This is the admin role.";

            await _roleManager.CreateAsync(adminRole);

            await _roleManager.AddClaimAsync(adminRole, new Claim("Can add roles", "add.role"));
            await _roleManager.AddClaimAsync(adminRole, new Claim("Can delete roles", "delete.role"));
            await _roleManager.AddClaimAsync(adminRole, new Claim("Can edit roles", "edit.role"));

            await _userManager.AddToRoleAsync(newAdminUser, adminRole.Name);

            //create user and basic role

            ApplicationUser basicUser = new ApplicationUser();

            basicUser.Email = "[email protected]";
            basicUser.UserName = "Basic";

            var resultBasic = await _userManager.CreateAsync(basicUser, "Password-1");

            var newBasicUser = await _userManager.FindByEmailAsync(basicUser.Email);

            ApplicationRole basicRole = new ApplicationRole();

            basicRole.Name = "Basic";
            basicRole.Description = "This is the basic role.";

            await _roleManager.CreateAsync(basicRole);

            //await _roleManager.AddClaimAsync(basicRole, new Claim("Can add roles", "add.role"));
            //await _roleManager.AddClaimAsync(basicRole, new Claim("Can delete roles", "delete.role"));
            //await _roleManager.AddClaimAsync(basicRole, new Claim("Can edit roles", "edit.role"));

            await _userManager.AddToRoleAsync(newBasicUser, basicRole.Name);

            await _context.SaveChangesAsync();
        }

    }
 }
}

Startup.CS: After creating my users, roles and claims (and associating them), I needed to register the 'Policies' in the Startup.cs class Confirgure Services method. This allows me to map the claims to a Policy or Policies.

 public void ConfigureServices(IServiceCollection services)
    {
        // Add framework services.
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        services.AddIdentity<ApplicationUser, ApplicationRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();


        services.AddAuthorization(options =>
        {
            options.AddPolicy("Add Role",
                policy => policy.RequireClaim("Can add roles", "add.role"));
            options.AddPolicy("Edit Role",
                policy => policy.RequireClaim("Can edit roles", "edit.role"));
            options.AddPolicy("Delete Role",
                policy => policy.RequireClaim("Can delete roles", "delete.role"));
        });

        services.AddMvc();

        services.AddTransient<DbInitializer>();

        // Add application services.
        services.AddTransient<IEmailSender, AuthMessageSender>();
        services.AddTransient<ISmsSender, AuthMessageSender>();
    }

View: In my use case, I wanted to restrict the 'Add Role' button from any user that doesn't have the "Can add roles" claim associated with the role they are assigned to. The rest of the view code is not relevant. The issue I ran into is that I passed in the claim name to the AuthorizationService.AuthorizeAsync as a second parameter vs the 'Policy' name which has the claim associated to it. I've since corrected it below.

@model IEnumerable<ApplicationRoleListViewModel>
@using HailMarry.Models
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService

<br />
<div class="top-buffer"></div>
<div class="panel panel-primary">
<div class="panel-heading panel-head">Application Roles</div>
<div class="panel-body">
    <div class="btn-group">

         //Mistake
        //@if (await AuthorizationService.AuthorizeAsync(User, "Can add roles"))
         //Fix
         @if (await AuthorizationService.AuthorizeAsync(User, "Add Role"))
        {
            <a id="createRoleButton" asp-action="AddRole" asp-controller="ApplicationRole" class="btn btn-primary">
                <i class="glyphicon glyphicon-plus"></i>  Add Role
            </a>
        }
....

End Result: I have a user "[email protected]" which is assigned to a role "Admin" which has a claim "Can add roles". A role can have any number of claims. I created a Policy which has the same claim "Can add roles" that I checked in the View via the injectable IAuthorizationService AuthorizationService. If the user does not have this claim assigned to their role, then the policy check which returns true or false will not show the button to add the role. This same policy checking logic can be added to a controller, or any other resource via DI thanks to the new .net core DI middleware. Through this entire exercise, I learned the power of Identity 3, which can harness things like business logic checking. Pretty sweet stuff, although the writers out there really need more examples to help us get to the meat faster. Anyhow, hope this helps future developers looking for a similar solution.

Answer

JReam picture JReam · Feb 8, 2017

I found the issue, I referenced the claim 'name' vs the policy name in the view...

I'll add notes above to illustrate the mistake and to show what I'm doing. Pretty powerful stuff, thanks to Ben and ASP.Net for an improved authorization solution over 4.5.