In this part we will configure Identity with Role based authorisation so that only an admin user can Update, Create and Delete products. This is not functionality that standard users should be able to use.

Lets start with adding a new class for the Role called AppRole.cs:

using Microsoft.AspNetCore.Identity;

namespace Core.Entities.Identity
{
    public class AppRole : IdentityRole
    {
    }
}

We don’t need to add any properties to this, just ensure it inherits from the IdentityRole class.

Next, we need to adjust the AppIdentityDbContext to include the new AppRole and we also need to specify the Id type here as well. Since we are sticking with the defaults this will mean that we use a string as the Id for these Identity classes:

using Core.Entities.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Infrastructure.Identity
{
    public class AppIdentityDbContext : IdentityDbContext<AppUser, AppRole, string>
    {
        public AppIdentityDbContext(DbContextOptions<AppIdentityDbContext> options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder) 
        {
            base.OnModelCreating(builder);
        }
    }
}

Just a small change here for the types. Since we already have the tables configured with .Net Core Identity and the tables there is no need to add a new migration here. Next let’s update the IdentityServiceExtensions.cs class. We need to add the RoleManager services here:

            var builder = services.AddIdentityCore<AppUser>();

            builder = new IdentityBuilder(builder.UserType, typeof(AppRole), builder.Services);
            builder.AddEntityFrameworkStores<AppIdentityDbContext>();
            builder.AddSignInManager<SignInManager<AppUser>>();
            builder.AddRoleValidator<RoleValidator<AppRole>>();
            builder.AddRoleManager<RoleManager<AppRole>>();

We will also update the AppIdentityDbContextSeed.cs class – the goal here is to seed an admin user and 2 roles that users can be populated into. We will use the seed data to seed an Admin user and populate this user into the Admin role – this user will be the only one that can add, update and delete products from the database. The full code for the new seed class is:

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Core.Entities.Identity;
using Microsoft.AspNetCore.Identity;

namespace Infrastructure.Identity
{
    public class AppIdentityDbContextSeed
    {
        public static async Task SeedUsersAsync(UserManager<AppUser> userManager, RoleManager<AppRole> roleManager)
        {
            if (!userManager.Users.Any())
            {
                var users = new List<AppUser>
                {
                    new AppUser
                    {
                        DisplayName = "Bob",
                        Email = "bob@test.com",
                        UserName = "bob@test.com",
                        Address = new Address
                        {
                            FirstName = "Bob",
                            LastName = "Bobbity",
                            Street = "10 The Street",
                            City = "New York",
                            State = "NY",
                            Zipcode = "90210"
                        }
                    },
                    new AppUser
                    {
                        DisplayName = "Admin",
                        Email = "admin@test.com",
                        UserName = "admin@test.com"
                    }
                };

                var roles = new List<AppRole>
                {
                    new AppRole {Name = "Admin"},
                    new AppRole {Name = "Member"}
                };

                foreach (var role in roles)
                {
                    await roleManager.CreateAsync(role);
                }

                foreach (var user in users)
                {
                    await userManager.CreateAsync(user, "Pa$$w0rd");
                    await userManager.AddToRoleAsync(user, "Member");
                    if (user.Email == "admin@test.com") await userManager.AddToRoleAsync(user, "Admin");
                }
            }
        }
    }
}

We will get an error in program.cs at this point – since we are using the RoleManager service here we also need to pass this as a parameter to the seed class so update the Program.cs class to do this:

var userManager = services.GetRequiredService<UserManager<AppUser>>();
                    var roleManager = services.GetRequiredService<RoleManager<AppRole>>();
                    var identityContext = services.GetRequiredService<AppIdentityDbContext>();
                    await identityContext.Database.MigrateAsync();
                    await AppIdentityDbContextSeed.SeedUsersAsync(userManager, roleManager);

Now that we have users in roles (or we will do when we drop the DB and restart the app), we also want to add the roles the user belongs to in the JWT token as a new claim. We will use the async version of the GetRolesAsync method provided by the user manager so we will need to update the ITokenService interface to return a task:

namespace Core.Interfaces
{
    public interface ITokenService
    {
         Task<string> CreateToken(AppUser user);
    }
}

Then the TokenService implentation class we add the new claims here. We will need to inject the UserManager service so we can access the GetRolesAsync method:

using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Core.Entities.Identity;
using Core.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;

namespace Infrastructure.Services
{
    public class TokenService : ITokenService
    {
        private readonly IConfiguration _config;
        private readonly UserManager<AppUser> _userManager;
        private readonly SymmetricSecurityKey _key;
        public TokenService(IConfiguration config, UserManager<AppUser> userManager)
        {
            _config = config;
            _userManager = userManager;
            _key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Token:Key"]));
        }

        public async Task<string> CreateToken(AppUser user)
        {
            var claims = new List<Claim>
            {
                new Claim(JwtRegisteredClaimNames.Email, user.Email),
                new Claim(JwtRegisteredClaimNames.GivenName, user.DisplayName)
            };

            var roles = await _userManager.GetRolesAsync(user);
            
            claims.AddRange(roles.Select(role => new Claim(ClaimTypes.Role, role)));

            var creds = new SigningCredentials(_key, SecurityAlgorithms.HmacSha512Signature);

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Expires = DateTime.Now.AddDays(7),
                SigningCredentials = creds,
                Issuer = _config["Token:Issuer"]
            };

            var tokenHandler = new JwtSecurityTokenHandler();

            var token = tokenHandler.CreateToken(tokenDescriptor);

            return tokenHandler.WriteToken(token);
        }
    }
}

When a new user registers we will want to add them to the “Members” role when they sign up – we are not doing anything with this role as everyone is a member, but just as an example of how to do so here is the updated Register method in the AccountController:

[HttpPost("register")]
        public async Task<ActionResult<UserDto>> Register(RegisterDto registerDto)
        {
            if (CheckEmailExistsAsync(registerDto.Email).Result.Value)
            {
                return new BadRequestObjectResult(new ApiValidationErrorResponse{Errors = new []{"Email address is in use"}});
            }

            var user = new AppUser
            {
                DisplayName = registerDto.DisplayName,
                Email = registerDto.Email,
                UserName = registerDto.Email
            };

            var result = await _userManager.CreateAsync(user, registerDto.Password);

            if (!result.Succeeded) return BadRequest(new ApiResponse(400));

            var roleAddResult = await _userManager.AddToRoleAsync(user, "Member");
            
            if (!roleAddResult.Succeeded) return BadRequest("Failed to add to role");

            return new UserDto
            {
                DisplayName = user.DisplayName,
                Token = await _tokenService.CreateToken(user),
                Email = user.Email
            };
        }

We also want to make sure that only the Admin user can use the Create, Update and Delete methods in the Product controller. The simplest method of doing this is to use the Authorize attribute and specify which role is allowed to use as follows so add the following attribute to each of the new CUD methods we added earlier:

        [HttpPost]
        [Authorize(Roles = "Admin")]

For any user that attempts to use these methods that is not in the Admin role, they will receive a 403 forbidden response from the API so lets add a new error message in the APIResponse.cs class in the Errors folder to accommodate this, sticking with the Yoda naming conventions:

        private string GetDefaultMessageForStatusCode(int statusCode)
        {
            return statusCode switch
            {
                400 => "A bad request, you have made",
                401 => "Authorized, you are not",
                403 => "Forbidden from doing this, you are",
                404 => "Resource found, it was not",
                500 => "Errors are the path to the dark side. Errors lead to anger.  Anger leads to hate.  Hate leads to career change",
                _ => null
            };
        }

In order to seed the new users we will need to drop the Identity database and then restart the app so we just need to run the following commands:

dotnet ef database drop -p Infrastructure -s API -c AppIdentityDbContext
dotnet run

Then we should see the new users being seeded in the console. We should now be able to test this in Postman. First, lets log in as bob and then check the token in jwt.io:

Then in jwt.io we can see that bob is in the member role:

If we login as the Admin user and do the same we can see that this user is in both the admin and member roles:

So we can see that the admin roles are returned as an array with both the Admin and Member roles – just as we would expect.

Now when we test the methods in postman we should find that any of the CUD operations for the products will result in a:

  • 401 for anonymous users
  • 403 for bob
  • 200 for admin

Ok thats it from the API side of things. In the next part we will configure the Angular app to hide things from non admin users.

The code for this part is contained in this commit on GitHub here.