Now with basic user management and authentication in place, we can set up Authorization with Roles. That is an authenticated User might have or not have the right to access different parts of the application, based on the membership of a Role.

Examples are that only some users are allowed to access the User administration or some sensitive information like payrolls etc.

What this post will cover

  • Create Roles with a RoleController
  • Use of Authorize Attribute with Role parameter

What this post will not cover

  • Setting up Asp Identity
  • User management
  • Authentication

Authorization with Roles

The Authorize Attribute is not only used to indicate to MVC that a user must be authenticated, but can also be used to refine Authorization. This is done by using a parameter of Role, which is then evaluated based on membership of the given user to this Role.

A Role represents permissions to perform a set of activities. You can think of it like some sort of label. e.g. Administrative privileges vs “normal” user.

AspNet core Identity manages the set of roles that can be defined in the configuration section. Asp Identity tracks which user belongs to which role. Yet the assertion of membership is done by the MVC part, because Identity does not know anything about the semantics of a Role.

The management is done by the RoleManager<T> class. T is the Type of Role that is stored in the application. By default Entity Framework Core uses the IdentityRole class that has the following properties:

  • Id: unique id
  • Name: role name
  • Users: collection of IdentityUserRole objects ( all members of this role )

We can see the use of IdentityRole with the call in the Startup class:

services.AddIdentity<MyUser, IdentityRole>(options => ...);

The type parameters define what is used for Users and Roles. Here we used the custom MyUser (from the User Management post) and the Asp built in IdentityRole, as it is the default value. You could easily customize the IdentityRole, but we will not do this in this simple introduction.

As with Usermanagement and the Repository style UserManager<T> class it exists a RoleManager<T> class that supports CRUD operations for the Roles of your application. It has also two additional Members. The first one is RoleExistsAsync(name), that checks if a Role with a given name is already defined. The other is simply Roles which returns an enumeration of all Roles in the system.

To let your Users create and delete Roles we’ll need a controller that is aptly called RoleController:

public class RoleController : Controller
{
private RoleManager<IdentityRole> _roleManager;
public RoleController(RoleManager<IdentityRole> roleManager)
=> _roleManager = roleManager;
public ViewResult Index() => View(roleManager.Roles);
[HttpPost]
public async Task<IActionResult> Create([Required] string name)
{
if (ModelState.IsValid)
{
IdentityResult result = await _roleManager.CreateAsync(new IdentityRole(name));
if (result.Succeeded)
return RedirectToAction("Index");
else
foreach (IdentityError error in result.Errors) {
ModelState.AddModelError("", error.Description);
}
return View(name);
}
[HttpPost]
public async Task<IActionResult> Delete(string id)
{
var role = await _roleManager.FindByIdAsync(id);
if (role != null)
{
IdentityResult result = await roleManager.DeleteAsync(role);
if (result.Succeeded) 
return RedirectToAction("Index");
else
foreach (IdentityError error in result.Errors) {
ModelState.AddModelError("", error.Description);
}
else
ModelState.AddModelError("", "No role found");
return View("Index", roleManager.Roles);
}
}

The Index method of the controller simply displays all the Roles in a View, (which I omitted here). We can see the same schema with Create and Delete from the RoleManager<T> class that acts as sort of repository. Then the validation etc. is very similar as we saw already with the AdminController (User Management) as it returns an IdentityResult object instance.

This is why I will not iterate again over the process.

Managing Role memberships

For the next part we will look at how we can associate Roles with Users to form Memberships in a given role.

This means essentially adding and removing Users to and from Roles. This is quite easy to do. You simply take RoleManager’s Role data and associate it with a User by adding it to the Roles collection. We will use a sort of DTO Model for this to work with Asp’s model binding.

For this we create in the Models namespace the following RoleState and RoleModification classes.
The RoleState is used to supply the given Role for a GET style operation. It has the IdentityRole, and a collection of Members and NonMembers.

The RoleModification class is used for modifying or creating a Role (so think Post/Put). It is constituted of the RoleName (which is used to query in the RoleManager<T> repository), a RoleId, and string[ ] arrays for RoleNames to add or delete.

public class RoleState
{
public IdentityRole Role { get; set; }
public IEnumerable<MyUser> Members { get; set; }
public IEnumerable<MyUser> NonMembers { get; set; }
}
public class RoleModification
{
[Required]
public string RoleName { get; set; }
public string RoleId { get; set; }
public string[] IdsToAdd { get; set; }
public string[] IdsToDelete { get; set; }
}

To put this to use, we add to the RoleController:

...
private readonly UserManager<MyUser> _userManager;
public RoleController(RoleManager<IdentityRole> roleManager, UserManager<MyUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
// GET
public async Task<IActionResult> Get(string id)
{
IdentityRole role = await _roleManager.FindByIdAsync(id);
var members = new List<MyUser>();
var nonMembers = new List<MyUser>();
foreach (MyUser user in userManager.Users)
{
var list = await _userManager.IsInRoleAsync(user, role.Name) ? members : nonMembers;
list.Add(user);
} 
return View(new RoleState
{
Role = role,
Members = members,
NonMembers = nonMembers
});
}
[HttpPost]
public async Task<IActionResult> Edit(RoleModification model)
{
IdentityResult result;
if (ModelState.IsValid)
{
foreach (string userId in model.IdsToAdd ?? new string[] { })
{
MyUser user = await _userManager.FindByIdAsync(userId);
if (user != null)
{
result = await _userManager.AddToRoleAsync(user, model.RoleName);
if (!result.Succeeded)
AddErrorsFromResult(result);
}
}
foreach (string userId in model.IdsToDelete ?? new string[] { })
{
MyUser user = await _userManager.FindByIdAsync(userId);
if (user != null)
{
result = await _userManager.RemoveFromRoleAsync(user, model.RoleName);
if (!result.Succeeded)
AddErrorsFromResult(result);
}
}
}
if (ModelState.IsValid)
return RedirectToAction(nameof(Index));
else
return await Edit(model.RoleId);
}
private void AddErrorsFromResult(IdentityResult result)
{
foreach (IdentityError error in result.Errors)
ModelState.AddModelError("", error.Description);
}

Note that we inject the UserManager<T> that also can work on Roles :

  • AddToRoleAsync(user, name)
  • GetRoleAsync(user)
  • IsInRoleAsync(user, name)
  • RemoveFromRoleAsync(user, name)

The Get action method with an Id as a parameter is a GET method and works by finding the IdentityRole associated with the injected Id. It then iterates over all the Users of the application and checks which are in the given Role and which are not. If so it adds them to the Members list, if not to the NonMembers. The above created RoleState DTO is then returned to a View that renders those collections in an appropriate manner. (again html is omitted for brevity)

The Edit method that is invoked on Post is used to change a given Role. This is done by evaluating the ModelState and then evaluating which roles should be added or removed. To do so we use the UserManager<T> class that looks for a given user and adds them/removes them via the AddTo/RemoveFrom RoleAsync methods. Again as is common with AspNet core Identity, the Result is then checked for success and else the errors are added to the ModelState for display.

Note that we again use the common Post/Redirect/Get pattern when the ModelState is still valid at the end of the operation.

Note also that the Add/Remove methods use RoleNames even though the IdentityRole class has an Id. (which deviates from the other repository style _Manager classes).

Authorization with roles

With Users and Roles setup we can now Authorize Users for roles on our Controllers.

For this we apply the Authorize attribute with a value to the Roles parameter.
I will demonstrate this on the HomeController (from the Usermanagement post).

HomeController:

[Authorize(Roles = "Users")]
public IActionResult MyAuthAction() => View("Index", GetData(nameof(MyAuthAction));
private Dictionary<string, object> GetData(string actionName)
=> new Dictionary<string, object>
{
["Action"] = actionName,
["User"] = HttpContext.User.Identity.Name,
["Authenticated"] = HttpContext.User.Identity.IsAuthenticated,
["Auth Type"] = HttpContext.User.Identity.AuthenticationType,
["In Users Role"] = HttpContext.User.IsInRole("Users")
};
// standard redirect on not authorized users
[AllowAnonymous]
public IActionResult AccessDenied()
=> View();

Now this is all fine and dandy, but the restriction to create a User or a Role are not in place. This is known as a chicken-egg-problem (or bootstraping problem).
Or in other words to use Authorize we need to be logged in, but without a user we cannot login.
The Solution to this problem is to seed the AspIdentity database with some initial data.

Essentially you could add an AdminUser that is allowed to create new Users and such.
This can be done in the app settings.json file for instance:

{
"Data": {
"AdminUser": {
"Name": "Admin",
"Email": "admin@example.com",
"Password": "password",
"Role": "Admins"
},
...
}

This data allows for the values that are needed to create an account and assign it to a Role that is allowed to use the administration tools.

To seed the database initially we have different options, but I choose for the presentations sake a simple solution with a static CreateAdminAsync method in the IdentityDbContext class:

public static async Task CreateAdminAccount(IServiceProvider serviceProvider,
IConfiguration configuration) 
{
UserManager<AppUser> userManager =
serviceProvider.GetRequiredService<UserManager<AppUser>>();
RoleManager<IdentityRole> roleManager =
serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string username = configuration["Data:AdminUser:Name"];
string email = configuration["Data:AdminUser:Email"];
string password = configuration["Data:AdminUser:Password"];
string role = configuration["Data:AdminUser:Role"];
if (await userManager.FindByNameAsync(username) == null) 
{
if (await roleManager.FindByNameAsync(role) == null) 
{
await roleManager.CreateAsync(new IdentityRole(role));
}
AppUser user = new AppUser 
{
UserName = username,
Email = email
};
IdentityResult result = await userManager.CreateAsync(user, password);
if (result.Succeeded) 
{
await userManager.AddToRoleAsync(user, role);
}
}
}

With the serviceProvide we get the User- and RoleManager classes that are needed to setup the Admin properly. Also we inject the IConfiguration to obtain the values provided in the config file.

Then we can use this mehtod in the Startup.cs like so:

public void Configure(IApplicationBuilder app) 
{
// other configurations omitted for brevity
AppIdentityDbContext.CreateAdminAccount(app.ApplicationServices,
Configuration).Wait();
}

Also because we use the ApplicationService that is scoped, we need to set the ValidateScopes option to false in the WebHostBuilder:

...
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseDefaultServiceProvider(options =>
options.ValidateScopes = false)
.Build();
...

With all this in place we can setup the Authorize Attribute on the Admincontroller like so:

[Authorize(Roles = "Admins")]
public class AdminController : Controller {
// ...statements omitted for brevity... }

The same can be done for the RoleController

[Authorize(Roles = "Admins")]
public class RoleController : Controller
{
// ...statements omitted for brevity...
}

Summary

In this post we saw how one can use the Authorize Attribute for Authorization in AspNet core Identity.

First we set up a RoleController and Model classes that can create and edit the Roles in the application. Then we saw, that we need to solve the bootstrapping problem that this entails and how we can overcome this by seeding the database.


0 Comments

Leave a Reply

Avatar placeholder