Authentication and Permission Based Authorization in ASP.NET 5 MVC – Part 03

In  this series, we learned till now

 

Hi, In previous part, we learned implementing login, registration, basic authorization. Today, we will see how to seed default user, default roles in database and add hardcoded permissions to default super role. Seeding something to database means, when application first runs, it will try to insert some data to database that are required. Default user will be a super user. The user will have all roles. So, all roles will be added to super user. Among the roles, there will be role SuperAdmin . SuperAdmin role will have all permissions.

Open Program.cs modify the following line CreateHostBuilder(args).Build().Run(); to CreateHostBuilder(args).Build().MigrateAndSeed().Run();

Create a file and name it IdentityMigrationManager.cs . Add migration and seed code to the file and it will look like following.

using Identity.Constants;
using Identity.Data;
using Identity.Models;
using Identity.Permissions;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

namespace Identity
{
    public static class IdentityMigrationManager
    {
        public static IHost MigrateAndSeed(this IHost host)
        {
            MigrateDatabaseAsync(host).GetAwaiter().GetResult();
            SeedDatabaseAsync(host).GetAwaiter().GetResult();
            return host;
        }

        public static async Task MigrateDatabaseAsync(IHost host)
        {
            using var scope = host.Services.CreateScope();
            await using var identityContext = scope.ServiceProvider.GetRequiredService<IdentityContext>();
            try
            {
                await identityContext.Database.MigrateAsync();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message + ". " + ex.Source);
                throw;
            }
        }

        public static async Task SeedDatabaseAsync(IHost host)
        {
            using var scope = host.Services.CreateScope();
            try
            {
                var userManager = scope.ServiceProvider.GetRequiredService<UserManager<AppUser>>();
                var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<AppRole>>();
                await SeedDefaultUserRolesAsync(userManager, roleManager, PermissionHelper.GetAllPermissions());
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message + ". " + ex.Source);
                throw;
            }
        }

        private static async Task SeedDefaultUserRolesAsync(UserManager<AppUser> userManager, RoleManager<AppRole> roleManager, List<Claim> permissions)
        {
            var defaultRoles = DefaultApplicationRoles.GetDefaultRoles();
            if (!await roleManager.Roles.AnyAsync())
            {
                foreach (var defaultRole in defaultRoles)
                {
                    await roleManager.CreateAsync(defaultRole);
                }
            }
            if (!await roleManager.RoleExistsAsync(DefaultApplicationRoles.SuperAdmin))
            {
                await roleManager.CreateAsync(new AppRole(DefaultApplicationRoles.SuperAdmin));
            }
            var defaultUser = DefaultApplicationUsers.GetSuperUser();
            var userByName = await userManager.FindByNameAsync(defaultUser.UserName);
            var userByEmail = await userManager.FindByEmailAsync(defaultUser.Email);
            if (userByName == null && userByEmail == null)
            {
                await userManager.CreateAsync(defaultUser, "SuperAdmin");
                foreach (var defaultRole in defaultRoles)
                {
                    await userManager.AddToRoleAsync(defaultUser, defaultRole.Name);
                }
            }

            var role = await roleManager.FindByNameAsync(DefaultApplicationRoles.SuperAdmin);
            var rolePermissions = await roleManager.GetClaimsAsync(role);
            var allPermissions = permissions;
            foreach (var permission in allPermissions)
            {
                if (rolePermissions.Any(x => x.Value == permission.Value && x.Type == permission.Type) == false)
                {
                    await roleManager.AddClaimAsync(role, permission);
                }
            }
        }
    }
}

I will try to explain what we did in above code. First we initiated MigrateDatabaseAsync. it is responsible for migrating pending migrations. Then initiated SeedDatabaseAsync method. Seed method is responsible for seeding default user, default roles, add all permissions to superAdmin role.

If you see closely in above code, there are some dependency classes that are not yet available in our code. We will add them one by one.

Under project root folder, create a folder and name it Constants. Inside the folder create a class and name it DefaultApplicationRoles.cs . After adding our code, it will look like this.

using Identity.Models;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;

namespace Identity.Constants
{
    public static class DefaultApplicationRoles
    {
        public const string SuperAdmin = "SuperAdmin";
        public const string Admin = "Admin";
        public const string Moderator = "Moderator";
        public const string Basic = "Basic";

        public static List<AppRole> GetDefaultRoles()
        {
            var roles = new List<AppRole>
            {
                new(SuperAdmin),
                new(Admin),
                new(Moderator),
                new(Basic)
            };
            return roles;
        }

        public static List<Claim> GetDefaultRoleClaims()
        {
            var roles = GetDefaultRoles();
            var claims = roles.Select(role => new Claim(ClaimTypes.Role, role.Name)).ToList();
            return claims;
        }
    }
}

Add another file under Constants & name is DefaultApplicationUser.cs and make the code look like following

using Identity.Models;

namespace Identity.Constants
{
    public class DefaultApplicationUsers
    {
        public static AppUser GetSuperUser()
        {
            var defaultUser = new AppUser
            {
                Id = "39c64ad6-39ae-4de0-a0b2-9298a46b4b4c",
                UserName = "SuperAdmin",
                Email = "a2masum@yahoo.com",
                FirstName = "Al",
                LastName = "Masum",
                EmailConfirmed = true,
                PhoneNumberConfirmed = true,
            };
            return defaultUser;
        }
    }
}

Obviously you can put your own information in above two files. 😛

Inside the IdentityMigrationManager.cs file, you would see another dependency that we haven’t worked yet that is PermissionHelper.GetAllPermissions() . This one retrieve all permissions that are hard coded for all modules in our application. For example, there is a module Blog . So, for Blog module there can be 4types of permissions. Blog.View, Blog.Create, Blog.Edit, Blog.Delete. Lets work on this.

Create a folder under Identity Project and name it Permissions. Under the folder create 3 file CustomClaimTypes.cs, PermissionHelper.cs, Permissions.cs. Three of them after modifying should look like this respectively.

namespace Identity.Permissions
{
    public class CustomClaimTypes
    {
        public const string Permission = "Permission";
    }
}
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Claims;

namespace Identity.Permissions
{
    public static class PermissionHelper
    {
        private static List<Claim> _allPermissions = new();
        public static List<Claim> GetAllPermissions()
        {
            if (_allPermissions.Count > 0) return _allPermissions;
            var allPermissions = new List<Claim>();
            var permissionClass = typeof(Permissions);
            var allModulesPermissions = permissionClass.GetNestedTypes().Where(x => x.IsClass).ToList();
            foreach (var modulePermissions in allModulesPermissions)
            {
                var permissions = modulePermissions.GetFields(BindingFlags.Static | BindingFlags.Public);
                allPermissions.AddRange(permissions.Select(permission =>
                    new Claim(CustomClaimTypes.Permission, permission.GetValue(null)?.ToString())));
            }
            _allPermissions = allPermissions;
            return _allPermissions;
        }
    }
}
namespace Identity.Permissions
{
    public static class Permissions
    {
        public static class Users
        {
            public const string View = "Permissions.Users.View";
            public const string Create = "Permissions.Users.Create";
            public const string Edit = "Permissions.Users.Edit";
            public const string ManageRoles = "Permissions.Users.ManageRoles";
            public const string ManageClaims = "Permissions.Users.ManageClaims";
            public const string Delete = "Permissions.Users.Delete";
        }

        public static class Roles
        {
            public const string View = "Permissions.Roles.View";
            public const string Create = "Permissions.Roles.Create";
            public const string Edit = "Permissions.Roles.Edit";
            public const string Delete = "Permissions.Roles.Delete";
            public const string ManageClaims = "Permissions.Roles.ManageClaims";
        }
    }
}

Currently in our application, there are two kinds of entities or module. They are user & role. That is why, the permission.cs file contains permission information for Users & Roles. CustomClaimTypes.cs define permission claim type for our module based permission. PermissionHelper.cs class responsible for retrieving all module’s permissions.

Now build the project. If you run the application and once it run, look into database. You will see the defined user, roles in database in respective tables. Also the permissions are added in RoleClaims table against the given role.

As we have done implementing the core parts. We need visualization/interface/html parts to interact with what we have done till now. Lets work on them in next part. We will learn how manage roles as a superAdmin and assign/de-assign roles to other users. Also we will see how to assign module permission to specific role and to specific user.

The source code till now can be found here. To read next part, click here.

Category:
ASP.Net

Leave a Comment