Integrating Microsoft Identity Authorization into a Menu System

May 19th, 2017

Developing a menu system is easy. Attaching security is not. In today's post, I extend Identity to attach authorizations and permissions to menu items.

UPDATE: I've updated the menu system to work with ASP.NET Core 3.1 with MVC and EF Migrations.

Updating the Menu System to ASP.NET Core 3.1 with Microsoft Identity

Every web application needs navigation which is why I introduced a barebones menu system for any type of web application.

You could use this system to create a simple horizontal menu, vertical menu or build your own.

However, what if only a select few of our users are allowed to view certain menu items?

We don't want developers running amok in the security area, now do we?

Today, we'll extend Microsoft Identity to attach authorizations to menu items.

Setup

In my example, I will be using Identity 2.x instead of using 3.0.

For the majority of this project, I decided to follow John Atten's tutorial on using Integer keys instead of strings on the Identity models.

What an awesome resource! Thanks a ton, John (@xivSolutions).

Also, I like to isolate as much of Identity as possible so I can reuse the code for later with minimal changes. As a result, I placed all of the necessary code in an Identity folder in the root of my project.

Once I finished the post on converting strings to ints, I had the following Identity folder structure.

Everything here is a standard Identity setup, but I've inherited from the general IdentityXxxx classes to use the integers instead of GUIDs.

Where Are We Going?

First, we need a roadmap of what we're building.

Since each user (ApplicationUser) can have a 1-to-many roles (ApplicationRole) using the ApplicationUserRole table to join them, our best approach is to attach menu items to each role.

Once we have the menu items defined (MenuItems), each MenuItem will map to a role (ApplicationRoleMenu) with each ApplicationRoleMenu key mapping to permissions. The permissions will tell us what they can and can't do.

For example, if we have a menu item called "Security" and they are a developer, the permission to access the Security module should be removed.

We don't want the inmates running the asylum. ;-)

Each menu item will have permissions like Create, Update, Publish, Delete, View, and Upload.

For my applications, I always relate a CRUD (Create, Read, Update, Delete) to each menu option.

For example, let's say I click the menu management link. When I get to the menu management link, because of the URL, we now know what page we're on and they have permissions to perform certain CRUD operations on this page.

I added the Publish and Upload as possible permissions to other pages. They were strictly added for demonstration purposes.

Of course, you'll be able to easily add your own permissions once we're done.

Extending ApplicationRole

Since Identity uses Entity Framework, we can easily attach menu items to the role.

Identity/ApplicationRole.cs

public class ApplicationRole : IdentityRole<int, ApplicationUserRole>, IRole<int>
{
    public ApplicationRole() { }
    public ApplicationRole(string name)
        : this()
    {
        this.Name = name;
        MenuItems = new HashSet<ApplicationRoleMenu>();
    }

    public ICollection<ApplicationRoleMenu> MenuItems { get; set; }
}

We add our MenuItems collection property and initialize it in our constructor.

Visual Studio should notify you that ApplicationRoleMenu is not defined yet. Let's create that now.

Identity/ApplicationRoleMenu.cs

public class ApplicationRoleMenu
{
    public ApplicationRoleMenu()
    {
        Permissions = new HashSet<MenuPermission>();
    }

    [Key, Column(Order=1), DatabaseGenerated(DatabaseGeneratedOption.Identity)]     public virtual int Id { get; set; }
    [Column(Order = 2)]     public virtual int RoleId { get; set; }
    [Column(Order = 3)]     public virtual int MenuId { get; set; }
    public virtual ApplicationRole Role { get; set; }     public virtual MenuItem MenuItem { get; set; }
    public ICollection<MenuPermission> Permissions { get; set; } }

ApplicationRoleMenu has a primary key called Id and a collection of MenuPermissions. I wanted this table to join the major players so we can find out who has access to what menu item, which role (or roles) they are in, and what permissions do they have?

We're continuing down the chain and find out we are now missing the MenuPermission class.

Let's do it.

Identity/MenuPermission.cs

public class MenuPermission
{
    [Key, Column(Order = 1)]
    public virtual int RoleMenuId { get; set; }

    [Key, Column(Order = 2)]     public virtual int PermissionId { get; set; }
    public virtual Permission Permission { get; set; }     public virtual ApplicationRoleMenu RoleMenu { get; set; } }

The MenuPermission class is meant to point a Menu Item and a Role to what the user can and can't do.

The Permissions class/table is simple.

Our permissions table will look like the following:

Id Name
1 Create
2 View
3 Update
4 Delete
5 Publish
6 Upload

If we want to make the menu item visible, we would include the View permission and that's it.

The MenuPermission table would contain an Id of the RoleMenuId along with a number 2 for the View. If you wanted to Create and Update, the MenuPermission table would contain 3 records with the RoleMenuId and 1,2, and 3.

Identity/Permission.cs

public class Permission
{
    [Key, Required]
    public int Id { get; set; }

    [Required]     [StringLength(50)]     public string Name { get; set; } }

All of the classes are created for our menu items.

We Need a Manager

With all of these menu items and roles, we need a centralized place to retrieve our menu items by role.

I created a MenuManager to simplify things a little bit.

Identity/MenuManager.cs

/// <summary>
/// To create your own permission, add an entry to the Permissions table
/// and call GetMenuByUser() by passing in your own permission.
/// </summary>
public class MenuManager
{
    private readonly ApplicationDbContext _context;

    public MenuManager(ApplicationDbContext context)     {         _context = context;     }
    public ICollection<MenuItem> GetMenuByUser(ApplicationUser user,          System.Func<MenuPermission, bool> filterFunc = null)     {         if (user == null)         {             return new Collection<MenuItem>();         }
        // Get Ids.         var roleIds = user.Roles.Select(role => role.RoleId).ToList();
        // Enable eager-loading to retrieve our permissions as well.         var items = _context.MenuItems             .Include(menu => menu.Roles.Select(role => role.Permissions))             .Where(e => e.Roles.Any(roleMenu => roleIds.Contains(roleMenu.RoleId)));
        ICollection<MenuItem> records;         if (filterFunc == null)         {             records = items.Where(e => e.Roles.Any(f => f.Permissions.Any())).ToList();         }         else         {             records = items.Where(e => e.Roles.Any(f => f.Permissions.Any(filterFunc))).ToList();         }

        return records;     }

    public ICollection<MenuItem> GetAllByUser(ApplicationUser user)     {         return GetMenuByUser(user);     }


    public ICollection<MenuItem> GetViewableMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "View");     }
    public ICollection<MenuItem> GetCreateMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "Create");     }
    public ICollection<MenuItem> GetDeleteMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "Delete");     }
    public ICollection<MenuItem> GetUpdateMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "Update");     }
    public ICollection<MenuItem> GetUploadMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "Upload");     }
    public ICollection<MenuItem> GetPublishMenuItems(ApplicationUser user)     {         return GetMenuByUser(user, menuPermission => menuPermission.Permission.Name == "Publish");     }
}

The meat of this class is the GetMenuByUser method.

First, we check to see if the user is null because...well, they may not be logged in. If we don't know who they are, menu items are removed.

Of course, you could refactor this to perform a default grab of menu items for unauthorized users. When they log in, then you can display authorized menu items.

Next, since we have a valid user, we get a list of role id's for reference purposes later.

With Entity Framework, we want to perform an eager-loading on the classes. While getting the menu items, we also want the roles and permissions so we include...ahh, the Include method.

Next, we only grab the menu items in each of the users role with our Where clause and we'll use our Id's from earlier to filter the results.

I wanted to extend this class to work with any type of permission which is why I added the Func<MenuPermission, bool> parameter.

If you want to add your own permission type, add a record to the Permission table and then pass in menuPermission => menuPermission.Permission.Name == "MyPermission" to the GetMenuByUser(user, () => blah) and it would attach to all menu items.

Phew. Still with me?

Good.

Persistent Storage

Using code-first with Entity Framework covers our definition of each class and where it's stored.

In our ApplicationDbContext class, we override the OnModelCreating and include the standard entity definitions with our updated classes.

Identity/ApplicationDbContext.cs

public class ApplicationDbContext
    : IdentityDbContext<ApplicationUser, ApplicationRole, int,
        ApplicationUserLogin, ApplicationUserRole, ApplicationUserClaim>
{
    public ApplicationDbContext(): base("MenuDatabaseEntities")
    {
    }

    static ApplicationDbContext()     {         Database.SetInitializer(new ApplicationDbInitializer());     }
    public static ApplicationDbContext Create()     {         return new ApplicationDbContext();     }
    protected override void OnModelCreating(DbModelBuilder modelBuilder)     {         if (modelBuilder == null)         {             throw new ArgumentNullException("modelBuilder error");         }
        // Needed to ensure subclasses share the same table         var user = modelBuilder.Entity<ApplicationUser>().ToTable("AspNetUsers");         user.HasMany(u => u.Roles).WithRequired().HasForeignKey(ur => ur.UserId);         user.HasMany(u => u.Claims).WithRequired().HasForeignKey(uc => uc.UserId);         user.HasMany(u => u.Logins).WithRequired().HasForeignKey(ul => ul.UserId);         user.Property(u => u.UserName)             .IsRequired()             .HasMaxLength(256)             .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("UserNameIndex") { IsUnique = true }));
        // CONSIDER: u.Email is Required if set on options?         user.Property(u => u.Email).HasMaxLength(256);
        modelBuilder.Entity<ApplicationUserRole>().HasKey(r => new { r.UserId, r.RoleId }).ToTable("AspNetUserRoles");
        modelBuilder.Entity<ApplicationUserLogin>()             .HasKey(l => new { l.LoginProvider, l.ProviderKey, l.UserId })             .ToTable("AspNetUserLogins");
        modelBuilder.Entity<ApplicationUserClaim>().ToTable("AspNetUserClaims");
        var role = modelBuilder.Entity<ApplicationRole>().ToTable("AspNetRoles");         role.Property(r => r.Name)             .IsRequired()             .HasMaxLength(256)             .HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute("RoleNameIndex") { IsUnique = true }));         role.HasMany(r => r.Users).WithRequired().HasForeignKey(ur => ur.RoleId);

        /////////////////////////////////////////         // All of this is standard but using our entity types (ApplicationXxxxxx) instead of IdentityXxxxxx         // Ref: https://aspnetidentity.codeplex.com/SourceControl/latest#src/Microsoft.AspNet.Identity.EntityFramework/IdentityDbContext.cs         /////////////////////////////////////////
        /* Role-Menu Definitions */
        modelBuilder.Entity<MenuItem>().ToTable("AspNetMenu");         modelBuilder.Entity<MenuItem>()             .HasMany(e => e.Children)             .WithOptional(e => e.ParentItem)             .HasForeignKey(e => e.ParentId);
        modelBuilder.Entity<MenuItem>()             .HasMany(e => e.Roles)             .WithRequired(e => e.MenuItem)             .HasForeignKey(e => e.MenuId)             .WillCascadeOnDelete(false);
        modelBuilder.Entity<ApplicationRoleMenu>().ToTable("AspNetRoleMenu");
        modelBuilder.Entity<ApplicationRoleMenu>()             .HasMany(e => e.Permissions)             .WithRequired(e => e.RoleMenu)             .HasForeignKey(e => e.RoleMenuId)             .WillCascadeOnDelete(false);
    }
    public IDbSet<MenuItem> MenuItems { get; set; }
    public IDbSet<Permission> Permissions { get; set; } }

Notice I don't have a base.OnModelCreating? Because I don't want to use the default mapping with the IdentityXxxxx. I want to use our own onModelCreating with specific ApplicationXxxxx classes instead.

The source for the OnModelCreating for building the standard Identity classes and relationships are located at IdentityDbContext.cs on CodePlex.com (I know...it's going away. Grab it while you can).

We also included our definitions for our relationships with our new ApplicationRoleMenu and MenuItems along with our IDbSets of MenuItems and Permissions so we can access them from the context.

Plant some Seeds

Finally, we need some data for our system.

In our ApplicationDbContext, we have a static constructor where we set the initializer. This initializer creates our data when Identity entities are accessed. For example, you won't see a database until you try to log in.

For demonstration purposes, our ApplicationDbInitializer inherits from DropCreateDatabaseAlways<ApplicationDbContext>. When your schema is finally ready, inherit from CreateDatabaseIfNotExists<AppplicationDbContext>.

Identity/ApplicationDbInitializer.cs

public class ApplicationDbInitializer : CreateDatabaseIfNotExists<ApplicationDbContext>
{
    protected override void Seed(ApplicationDbContext context)
    {
        InitializeIdentityForEf(context);
        base.Seed(context);
    }

    public static void InitializeIdentityForEf(ApplicationDbContext context)     {         var userManager = new ApplicationUserManager(new ApplicationUserStore(context));         var roleManager = new ApplicationRoleManager(new ApplicationRoleStore(context));
        CreatePermissions(context);
        var menuItems = CreateMenu(context);
        var roles = CreateRoles(roleManager);
        // User         // +--Roles (Developer, Administrator, etc.)         //   +--MenuItem (each MenuItem a single user can use)         //     +--Permissions (what can the user do per menu item...create, update, delete, etc.)
        // Administrator User         var adminUser = CreateAdministrator(context, userManager);
        // Assign the role to the user.         var administrator = roles.FirstOrDefault(role => role == "Administrator");         userManager.AddToRoles(adminUser.Id, administrator);         context.SaveChanges();

        // Developer User         var devUser = CreateDeveloperUser(context, userManager);

        // Setup the developers.         var developer = roles.FirstOrDefault(e => e == "Developer");         userManager.AddToRoles(devUser.Id, developer);         context.SaveChanges();



        // Developer Role         var developerRole = roleManager.FindByName("Developer");         // Add all of the menuItems responsible for a developer         // (Everything except for security).         var menuItemsInDeveloper = menuItems             .Select(e => new ApplicationRoleMenu             {                 RoleId = developerRole.Id,                 MenuId = e.Id             });         foreach (var roleMenu in menuItemsInDeveloper)         {             developerRole.MenuItems.Add(roleMenu);             context.SaveChanges();
            // If it's not a security part of the system,              //   allow the developer to access everything.             if (roleMenu.MenuItem.Title == "Security") continue;
            // Adding the security             for (int i = 0; i < 6; i++)             {                 roleMenu.Permissions.Add(new MenuPermission { RoleMenuId = roleMenu.Id, PermissionId = i+1 });             }         }         context.SaveChanges();


        // Administrator Role.         var administratorRole = roleManager.FindByName("Administrator");         var menuItemsInAdministrator = menuItems             .Select(e => new ApplicationRoleMenu             {                 RoleId = administratorRole.Id,                 MenuId = e.Id             });         foreach (var roleMenu in menuItemsInAdministrator)         {             administratorRole.MenuItems.Add(roleMenu);             context.SaveChanges();             // Add ALL permissions for an Administrator.             for (int i = 0; i < 6; i++)             {                 roleMenu.Permissions.Add(new MenuPermission { RoleMenuId = roleMenu.Id, PermissionId = i + 1 });             }         }         context.SaveChanges();     }
    private static ApplicationUser CreateAdministrator(ApplicationDbContext db, ApplicationUserManager userManager)     {         // Create the administrator         var user = userManager.FindByEmail("bob@gmail.com");         if (user == null)         {             user = new ApplicationUser             {                 Email = "bob@gmail.com",                 UserName = "bob",                 EmailConfirmed = true             };             userManager.Create(user, "Password123!");         }         db.SaveChanges();
        return user;     }     private static ApplicationUser CreateDeveloperUser(ApplicationDbContext db, ApplicationUserManager userManager)     {         // Create the administrator         var user = userManager.FindByEmail("frank@gmail.com");         if (user == null)         {             user = new ApplicationUser             {                 Email = "frank@gmail.com",                 UserName = "frank",                 EmailConfirmed = true             };             userManager.Create(user, "Password456!");         }         db.SaveChanges();
        return user;     }
    private static string[] CreateRoles(ApplicationRoleManager roleManager)     {         string[] roles = {"Administrator", "Publisher", "Editor", "Developer", "Designer", "Copywriter"};         foreach (string role in roles)         {             if (roleManager.RoleExists(role)) continue;             var roleResult = roleManager.Create(new ApplicationRole(role));         }
        return roles;     }
    private static List<MenuItem> CreateMenu(ApplicationDbContext db)     {         // Seed the MenuItems         var menuItems = new List<MenuItem>         {             new MenuItem             {                 Id = 1,                 Title = "Setup",                 Description = "Setup the system",                 ParentId = null,                 Icon = "wrench",                 Url = null             },             new MenuItem             {                 Id = 2,                 Title = "Users",                 Description = "Manage Users",                 ParentId = 1,                 Icon = "user",                 Url = "/setup/users"             },             new MenuItem             {                 Id = 3,                 Title = "Security",                 Description = "Set Permissions for System",                 ParentId = 1,                 Icon = "lock",                 Url = "/setup/security"             },             new MenuItem             {                 Id = 4,                 Title = "Menu Management",                 Description = "Manage the Menus for the System",                 ParentId = 1,                 Icon = "list",                 Url = "/Setup/Menu"             }         };
        foreach (var menuItem in menuItems)         {             db.MenuItems.Add(menuItem);         }         db.SaveChanges();
        return menuItems;     }
    private static void CreatePermissions(ApplicationDbContext db)     {         // Create the permissions         var permissions = new List<Permission>         {             new Permission {Id = 1, Name = "Create"},             new Permission {Id = 2, Name = "View"},             new Permission {Id = 3, Name = "Update"},             new Permission {Id = 4, Name = "Delete"},             new Permission {Id = 5, Name = "Publish"},             new Permission {Id = 6, Name = "Upload"}         };         foreach (var permission in permissions)         {             db.Permissions.Add(permission);         }         db.SaveChanges();     } }

What we've created in our ApplicationDbInitializer is the following:

If you notice the developer role, we do not include the Security menu item in our list.

Again, this was merely for demonstration purposes. You can easily create a management screen to add, update, and delete user permissions.

Our Entry Point

Since this is an MVC application, I'm using the Home/Index page (same as the MenuSystem Demo from before) to view menu items.

Controllers/HomeController.cs

public ActionResult Index()
{
    var menuManager = new MenuManager(new ApplicationDbContext());

    var username = GetUserName();
    var userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
    var model = new MenuViewModel();
    var user = userManager.FindByName(username);     if (user != null)     {         model.MenuItems = menuManager.GetAllByUser(user).ToList();     }
    return View(model); }
[NonAction] private string GetUserName() {     return this.ControllerContext.HttpContext.User.Identity.Name; }

The MenuManager is instantiated with a new ApplicationDbContext and we get the UserName.

Once we find the username (bob or frank...or no one), we populate the menu items by using the GetAllByUser and pass the model over to the View.

Keep in mind, we didn't even touch the HtmlHelpers. That's the beauty of this.

What did we make?

So let's run our application.

When we first run the application, the user isn't authorized and no menu items are available for the user. For demonstration purposes, this was fine.

When we log in as Bob (who is an admin), he has access to the Security module.

If we log Bob off and log in as Frank, he isn't authorized to access the Security module, so the module isn't available.

Conclusion

In today's post, we achieved building a universal menu system with authorizations giving us code reuse in the long run for a number of different projects.

To extend this example further,

While this was a struggle to get Identity to work with me, I'm confident this routine will translate over to ASP.NET MVC Core relatively easily.

I say that now. ;-)

UPDATE: I've added another post making authorization easier using Claims in the Menu System.

Did I make it too complicated? Is there a different approach to building these menu items, roles, and permissions? Post a comment and let's discuss.