Role-Based Authentication in .NET Core 3.1

Patrick God - Aug 10 '20 - - Dev Community

This tutorial series is now also available as an online video course. You can watch the first hour on YouTube or get the complete course on Udemy. Or you just keep on reading. Enjoy! :)

Role-Based Authentication in .NET Core 3.1

Introduction

You have asked for it, so here it is: Role-based authentication.

In this chapter, you will learn how to add a role to a User, read that role, and use it to change the behavior of a service and extend the [Authorize] attribute.

To be more specific, we will change the behavior of the GetAllCharacters() method.

Currently, users can only see the characters they have created, but if a user has got the Admin role, this user will be able to see all the characters of every user.

To realize that, we first add a new property to the User class, run another migration, add a new Claim to the JSON web token, and then read that new Claim to decide what the user is allowed to see.

Let’s start.

New User Property: Role & A New Migration

First of all the User gets a new property called Role which is a string.

Additionally, we add an attribute to that property, and this attribute is called [Required].

To make this work, we need the System.ComponentModel.DataAnnotations using directive.

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public byte[] PasswordHash { get; set; }
    public byte[] PasswordSalt { get; set; }
    public List<Character> Characters { get; set; }
    [Required]
    public string Role { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

With the [Required] attribute, we tell Entity Framework that this property is not nullable. So we have to set a value for a role. Since this property is a string, the default value is simply an empty string.

Let’s change that and set the default value to “Player”. We do that in the DataContext class in the OnModelCreating() method.

For the User entity, we set the default value of the property Role with the method HasDefaultValue().

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<CharacterSkill>()
        .HasKey(cs => new { cs.CharacterId, cs.SkillId });

    modelBuilder.Entity<User>()
        .Property(user => user.Role).HasDefaultValue("Player");
}
Enter fullscreen mode Exit fullscreen mode

Now, when we run another migration, all the users that already exist get the “Player” role and every new user will get this role as well.

Let’s add another migration with dotnet ef migrations add Role.

As you can see in the Up() method of this new migration, a new column is added which is not nullable and with the default value “Player”. Great.

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AddColumn<string>(
        name: "Role",
        table: "Users",
        nullable: false,
        defaultValue: "Player");
}
Enter fullscreen mode Exit fullscreen mode

Let’s update the database with dotnet ef database update.

When this is done, we can see the new column and the “Player” role for every user in the database.

And when we register a new user with Postman, even the new user gets this role.

New Role Property

Perfect. This works so far. Now we have to add this role to the user’s token.

Extend the JSON Web Token with another Claim

Adding this role to the user’s token is actually quite simple. It’s just a new Claim we have to add.

So in the AuthRepository in the CreateToken() method, we add a new Claim to the claims list.

The type is ClaimTypes.Role and the value is the new Role property of the user object.

private string CreateToken(User user)
{
    List<Claim> claims = new List<Claim>
    {
        new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
        new Claim(ClaimTypes.Name, user.Username),
        new Claim(ClaimTypes.Role, user.Role)
    };
// ...
Enter fullscreen mode Exit fullscreen mode

That’s it.

But let’s see if this really worked.

We log a user in, copy the resulting token, and then use the debugger of jwt.io in the browser.

We can paste the token, and indeed we see the new claim. The role of the user is “Player”.

JWT

Nice. Let’s finish this new feature now, by changing the behavior of a service and restricting access to a specific role.

Restrict Controller Access & Change Service Behavior

The goal is to change the behavior of the GetAllCharacters() method in the CharacterService.

Before we do that, I want to show you how you can control access to a method or a whole class.

In the CharacterController, we’re already using the [Authorize] attribute. So, only authenticated users can access this controller.

To restrict access even further, we can simply add “Roles” to that attribute, for instance, the “Player” role.

[Authorize(Roles = "Player")]
[ApiController]
[Route("[controller]")]
public class CharacterController : ControllerBase
{
  // ...
Enter fullscreen mode Exit fullscreen mode

Now, this wouldn’t change anything for our authenticated testuser, so let’s give this user the role “Admin” in the database manually.

Admin Role

We have to log in the user again with Postman to get the new token and then we use this token to get all of the user’s characters.

But as you can see, this is not possible. We get a 403 Forbidden.

So let’s add the role “Admin” now to the [Authorize] attribute. We can do that by simply adding a comma and the second role.

[Authorize(Roles = "Player,Admin")]
Enter fullscreen mode Exit fullscreen mode

When we now want to see the user’s characters, it does work.

But let’s really make use of an “Administrator” role.

Simple “Players” can only see the characters they have created. An admin should see all the characters.

So let’s move to the CharacterService and add a little method called GetUserRole() similar to the method GetUserId().

Only this time the return type is a string and we want the value of the claim type Role.

private string GetUserRole() => _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.Role);
Enter fullscreen mode Exit fullscreen mode

Alright. And now in the GetAllCharacters() method we check if the user role is equal to “Admin” and if so, we return all characters, and if not, we use the old line where we filter the characters by the User.Id.

List<Character> dbCharacters = 
    GetUserRole().Equals("Admin") ? 
    await _context.Characters.ToListAsync() :
    await _context.Characters.Where(c => c.User.Id == GetUserId()).ToListAsync();
Enter fullscreen mode Exit fullscreen mode

When we test that again with Postman, we even get the characters of the other users. Exactly what we want.

So, this works fine, and that’s how you add role-based authentication.

As always, you can get the source code in the corresponding GitHub repository and if you have any further feature requests or questions, don’t hesitate to contact me.

See you next time!


Image created by cornecoba on freepik.com.


But wait, there’s more!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .