Click here to Skip to main content
15,860,859 members
Articles / Web Development / ASP.NET

Extending Identity Accounts and Implementing Role-Based Authentication in ASP.NET MVC 5

Rate me:
Please Sign up or sign in to vote.
4.94/5 (81 votes)
24 Jun 2014CPOL28 min read 427.5K   156   94
Implementing simple role-based identity management and working with identity accounts in ASP.NET MVC 5.

5555555-500In a previous post, we took a quick look at extending Identity Accounts in the context of the new Identity system under ASP.NET MVC 5. We also examined the basics of Entity Framework Code-First Migrations. If you haven't read that article, you might want to do so now, so we don't have to repeat some of the general ideas explained there.

Image by: Elif Ayiter | Some Rights Reserved

Note: This article was originally posted on my personal blog at http://typecastexception.com. There was an issue with my rss feed (still working on that) so it was not pulled automatically by the CD Technical Blog reader. For that reason, I manually migrated it here as an article. The content is my own, and has not been plagiarized in any way.

If you are using the Identity 2.0 Framework:

This article focuses on customizing and modifying version 1.0 of the ASP.NET Identity framework. If you are using the recently released version 2.0, this code in this article won't work. For more information on working with Identity 2.0, see ASP.NET Identity 2.0: Understanding the Basics and ASP.NET Identity 2.0: Setting Up Account Validation and Two-Factor Authentication.

Many of the customizations implemented in this article are included "ini the box" with the Identity Samples project. I discuss extending and customizing IdentityUser and IdentityRole in Identity 2.0 in a new article, ASP.NET Identity 2.0: Customizing Users and Roles

If you are using the Identity 1.0 Framework:

For the purpose of this post, we are going to look at a implementing relatively simple role-based authentication and identity management for an ASP.NET MVC 5 web application. The examples used will be deliberately simplified, and while they will effectively illustrate the basics of setting up role-based identity management, I can promise that the implementation here will lack certain things we would like to see in a production project (such as complete exception handling). Also, production application modeling would likely look a little different based upon business needs.

In other words, I'm keeping it simple, much like we ignored the effects of friction way back in high school physics. For a look at how tom implement more granular application permission management, see ASP.NET MVC 5 Identity: Implementing Group-Based Permissions Management.

That said, there's a lot to cover. The article is not as long as it seems, because I am including some large code samples, and images to illustrate what's going on.

Download the Source Code

1/28/2014 - IMPORTANT: Based on some of the comments below, I strongly recommend cloning the source for this article from Github. There are a lot of moving parts here, and it is too easy to miss some critical step.

The complete project source code for this article is available at my Gihub Repo. NOTE: You will need to enable Nuget Package Restore in order to build the project properly.

Basic Hypothetical Application Requirements

We will assume our identity management needs to do the following for a Line-of-Business web application used primarily by internal users or others suitably authorized by management. The application will have a minimal public-facing interface, and will require all users to log-in to access even minimal functionality (it is easy to extend what we will be doing to include public users, but this is derived from an actual application I needed to create rather quickly at work).

  • User accounts must be created by one or more Admin-level users. "Registration" as we know it from the ASP.NET Membership paradigm, does not exists (anonymous users cannot register and create accounts from the public site).
  • User Identity accounts will be extended to include an email address, and first/last names
  • For our purposes, there will be at least three Roles; Administrator (full access to everything), Editor (can perform most business functions of the application, but cannot access admin functions such as account management), and Read-Only User (what the name implies).
  • Each user may be a member of zero or more roles. Multiple roles will have the access rights of all the roles combines.
  • To keep things simple, roles are independently defined, and are not members of other roles.
  • All application roles are pre-defined. There is no administrative creation of roles. Also, Role permissions are integral to the application, and not manageable by administrators (this is for simplicity at this point).
  • Anonymous access to the site is not allowed, except to the log-in portal.
  • There will be no use of external log-ins or OAuth (code for this is included as part of the default MVC project; we will remove it to keep things clean).

In the above, I have purposely omitted administrative creation/deletion of roles. For the purpose of our example, this adds an unacceptably complex wrinkle. Because our access privileges in this case are going to be managed using [Authorize] attribute (in other words, with hard-coded values), adding or deleting roles, will create issues beyond the scope of our example. There are ways around this which I will discuss in a future article.

 

 

Getting Started – Stripping Unneeded Items from the MVC Project Template

While we could start with what we created in the previous article on Extending Identity Accounts, we will be re-arranging things sufficiently it will be cleaner for our purposes to start fresh. Create a new ASP.NET MVC project in Visual Studio. Before we do anything else, let's clear out some unneeded clutter so we are left with only what we need.

We are going to be removing a bunch of code related to managing external log-ins, as well as clearing out some of the extraneous comments included with the default project (which to me, just add noise to our code).

We will start with the AccountController.

Simplifying AccountController – Remove the Clutter

There are a number of methods on AccountController related to External Logins which we don't need. If you examine AccountController carefully, you can go through and delete the code for the following methods:

  • Dissociate()
  • ExternalLogin()
  • ExternalLoginCallback()
  • LinkLogin()
  • LinkLoginCallback()
  • ExternalLoginConfirmation()
  • ExternalLoginFailure()
  • RemoveAccountList()

Additionally, if you look closely, there is a code #region (I know. I hate #region and think it should be done away with) for helpers. From here, we can delete the following items:

  • The member constant XsrfKey
  • The entire class ChallengeResult

At this point, we can also right-click in the code file and select Remove and Sort Usings to clear out some of the unneeded clutter here as well.

At this point, our AccountController.cs file should contain the following methods (reduced to simple stubs here for brevity – we'll get to the code shortly:

The Cleaned Up AccountController.cs File (Stubs Only - Code Hidden for Brevity):
C#
using AspNetRoleBasedSecurity.Models;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin.Security;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
  
namespace AspNetRoleBasedSecurity.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController()
            : this(new UserManager<ApplicationUser>(
            new UserStore<ApplicationUser>(new ApplicationDbContext())))
        {
        }
  
  
        public AccountController(UserManager<ApplicationUser> userManager)
        {
            UserManager = userManager;
        }
  
  
        public UserManager<ApplicationUser> UserManager { get; private set; }
  
  
        [AllowAnonymous]
        public ActionResult Login(string returnUrl)
        {
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }
  
  
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [AllowAnonymous]
        public ActionResult Register()
        {
            return View();
        }
  
  
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Register(RegisterViewModel model)
        {
             // . . . Code Hidden for brevity
        }
  
  
        public ActionResult Manage(ManageMessageId? message)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<ActionResult> Manage(ManageUserViewModel model)
        {
             // . . . Code Hidden for brevity
        }
  
  
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("Index", "Home");
        }
   
  
        protected override void Dispose(bool disposing)
        {
             // . . . Code Hidden for brevity
        }
  
  
  
  
        #region Helpers
  
  
        private IAuthenticationManager AuthenticationManager
        {
             // . . . Code Hidden for brevity
        }
  
  
        private async Task SignInAsync(ApplicationUser user, bool isPersistent)
        {
             // . . . Code Hidden for brevity
        }
  
  
        private void AddErrors(IdentityResult result)
        {
             // . . . Code Hidden for brevity
        }
  
  
        private bool HasPassword()
        {
             // . . . Code Hidden for brevity
        }
  
  
        public enum ManageMessageId
        {
             // . . . Code Hidden for brevity
        }
  
  
        private ActionResult RedirectToLocal(string returnUrl)
        {
             // . . . Code Hidden for brevity
        }
  
  
        #endregion
    }
}

Remove Unneeded Views

Along with the unnecessary Controller methods we just removed, we can also remove the unnecessary views related to external logins. If we open the Views => Account folder in Solution Explorer, we find we can delete the highlighted views below from our project:

Solution Explorer – Remove Unneeded Views:

solution-explorer-remove-unneeded-views

Now that the totally unnecessary views are out of the way, let's remove the External Log-in code from the remaining views as well.

Clean Up Account-Related Views

There is some remaining clutter to clear out of our Account-related views. We don't want dead-end links on our site, and we want to keep only relevant code in our views.

We can start with the Login.cshtml file, which contains a section related to creating an external log-in from various social networks (highlighted in yellow).

Login.cshtml - Remove Social Network Login Option:
C#
@{
    ViewBag.Title = "Log in";
}

<h2>@ViewBag.Title.</h2>
<div class="row">
    <div class="col-md-8">
        <section id="loginForm">
            @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
            {
                @Html.AntiForgeryToken()
                <h4>Use a local account to log in.</h4>
                <hr />
                @Html.ValidationSummary(true)
                <div class="form-group">
                    @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.UserName)
                    </div>
                </div>
                <div class="form-group">
                    @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
                    <div class="col-md-10">
                        @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
                        @Html.ValidationMessageFor(m => m.Password)
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <div class="checkbox">
                            @Html.CheckBoxFor(m => m.RememberMe)
                            @Html.LabelFor(m => m.RememberMe)
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="col-md-offset-2 col-md-10">
                        <input type="submit" value="Log in" class="btn btn-default" />
                    </div>
                </div>
                <p>
                    @Html.ActionLink("Register", "Register") if you don't have a local account.
                </p>
            }
        </section>
    </div>
    <div class="col-md-4">
        <section id="socialLoginForm">
            @Html.Partial("_ExternalLoginsListPartial", new { Action = "ExternalLogin", ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

In the above, the entire <div> containing the <section> "socialLoginForm" (Near the very bottom) can be deleted.

Next, let's remove similar External Login functionality from the Manage.cshtml file (highlighted in yellow, again):

Manage.cshtml – Remove External Login Items:
C#
@using AspNetRoleBasedSecurity.Models;
@using Microsoft.AspNet.Identity;
@{
    ViewBag.Title = "Manage Account";
}
  
<h2>@ViewBag.Title.</h2>
  
<p class="text-success">@ViewBag.StatusMessage</p>
<div class="row">
    <div class="col-md-12">
        @if (ViewBag.HasLocalPassword)
        {
            @Html.Partial("_ChangePasswordPartial")
        }
        else
        {
            @Html.Partial("_SetPasswordPartial")
        }
  
        <section id="externalLogins">
            @Html.Action("RemoveAccountList")
            @Html.Partial("_ExternalLoginsListPartial", new { Action = "LinkLogin", ReturnUrl = ViewBag.ReturnUrl })
        </section>
    </div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

As before, the <section> with id="externalLogins" (again, near the bottom) can be safely removed.

Remove Unneeded Model Classes

As with the previous sections, there are unneeded model classes we can safely dispose of in order to clean up our project. If we expand the Models folder in Solution Explorer, we find there is a single code file, AccountViewModels.cs, containing several ViewModel classes related to Identity Management. Review the file carefully, and delete the ExternalLogInConfirmationViewModel item below:

Account View Models File – Remove Unneeded Classes:
C#
using System.ComponentModel.DataAnnotations;

namespace AspNetRoleBasedSecurity.Models
{
    public class ExternalLoginConfirmationViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
    }
  
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }

        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}

Extending the Identity Management Models and View Models

As we have seen, the Model classes used by our application to manage identity and authorization are contained in the IdentityModels.cs file. Additionally, there are Identity-related ViewModel classes defined in the AccountViewModels.cs file used to manage the transfer of identity data between our views and controllers.

Important to note here that we would really like to get all of our model definitions correct before we run the application and create any new user accounts, or register using the normal (and soon-to-be-removed) MVC "registration" mechanism. We are going to use Entity Framework Migrations and Code-First to do the Database heavy-lifting for us. While it is not terribly difficult to update our models later (and hence, the database, through EF migrations), it is cleaner and smoother to get it right up front.

In order to conform to our project specifications, one of the first things we need to do is extend the default ApplicationUser class to include Email, LastName, and FirstName properties. Open the IdentityModels.cs file. Currently, the code should look like this:

The Default IdentityModels File with Emply ApplicationUser Stub:
C#
using Microsoft.AspNet.Identity.EntityFramework;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ApplicationUser : IdentityUser
    {
    }
  
  
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
        }
    }
}

In this step, we are going to extend our ApplicationUser class to include the properties required by our application specification. Also, we will add an IdentityManager class in which we consolidate our user and role management functions. We'll discuss how all this works in a moment. For now, add the following code to the IdentityModels.cs code file (note we have added some new using statements at the top as well):

Modified IdentityModels.cs File
C#
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ApplicationUser : IdentityUser
    {
        [Required]
        public string FirstName { get; set; }
   
        [Required]
        public string LastName { get; set; }
   
        [Required]
        public string Email { get; set; }
    }
    
  
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {
        public ApplicationDbContext()
            : base("DefaultConnection")
        {
            
        }
    }
  
  
    public class IdentityManager
    {
        public bool RoleExists(string name)
        {
            var rm = new RoleManager<IdentityRole>(
                new RoleStore<IdentityRole>(new ApplicationDbContext()));
            return rm.RoleExists(name);
        }
  
  
        public bool CreateRole(string name)
        {
            var rm = new RoleManager<IdentityRole>(
                new RoleStore<IdentityRole>(new ApplicationDbContext()));
            var idResult = rm.Create(new IdentityRole(name));
            return idResult.Succeeded;
        }
  
  
        public bool CreateUser(ApplicationUser user, string password)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var idResult = um.Create(user, password);
            return idResult.Succeeded;
        }
  
  
        public bool AddUserToRole(string userId, string roleName)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var idResult = um.AddToRole(userId, roleName);
            return idResult.Succeeded;
        }
  
  
        public void ClearUserRoles(string userId)
        {
            var um = new UserManager<ApplicationUser>(
                new UserStore<ApplicationUser>(new ApplicationDbContext()));
            var user = um.FindById(userId);
            var currentRoles = new List<IdentityUserRole>();
            currentRoles.AddRange(user.Roles);
            foreach(var role in currentRoles)
            {
                um.RemoveFromRole(userId, role.Role.Name);
            }
        }
    }
}

Yeah, I know. There is room for some refactoring here.We'll ignore that for now. In the above, we have extended ApplicationUser to include our new required properties, and added the IdentityManager class, which includes the methods required to create new users, and add/remove users from available roles.

We have also decorated our new ApplicationUser properties with the [Required] data annotation, which will be reflected both in our database (nulls will not be allowed) and our Model Validation for our views.

SIDE NOTE, REDUX: I am utilizing the methods available directly within the Microsoft.AspNet.Identity and Microsoft.AspNet.Identity.EntityFramework namespaces. I am content to let the ASP.NET team invent and provide the best practices for managing application security. Therefore, in the context of this application, I am not inventing my own. I strongly recommend you do as well. It is easy to spot ways to manage some of the Account/Identity stuff (including data persistence) which appear more direct or easier. I concluded that the team thought all this through more effectively than I can. Therefore, while we are creating an authorization management structure here, we are doing so using the core implementation provided by people who know better than we do.

Extending Account Management ViewModels

Now that we have expanded upon our basic Identity Models, we need to do the same with our Account ViewModels. ViewModels essentially represent a data exchange mechanism between our views and Controllers. Our goal here is to provide our Account management views with precisely the information required to perform the task at hand, and no more.

Also of note is that for the purpose of our presentation layer, I am not pushing User or Role id's out onto the page or the backing html. Instead I am relying on the unique nature of the Username and Role names to look up the proper id value server-side.

Currently, before we do anything, our AccountVeiwModels.cs file looks like this:

The AccountViewModels.cs File Before Modification:
C#
using System.ComponentModel.DataAnnotations;

namespace AspNetRoleBasedSecurity.Models
{
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = 
            "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = 
            "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
}

We are going to expand on this significantly. In fact, it may seem, redundantly. For, with a few minor differences, it might appear that one or two of the ViewModels in the following code are near-duplicates, and possible candidates for a refactoring into a single class. However, I decided that the purpose of the ViewModel is to represent the specific data required by a specific view. While in some cases these appear to be the same, that may change. I concluded that it is better, so far as Views and ViewModels go, to have some potential duplication, but preserve the ability of each view to evolve independently should the need arise, without having to fuss with the impact on other views dependent on the same ViewModel.

Modify the code above as follows (or simply replace it with the following). We'll take a closer look at the functionality in a moment, when we get to the Controller implementation:

Modified Code for AccountViewModels.cs File:
C#
using System.ComponentModel.DataAnnotations;
  
// New namespace imports:
using Microsoft.AspNet.Identity.EntityFramework;
using System.Collections.Generic;
  
namespace AspNetRoleBasedSecurity.Models
{
    public class ManageUserViewModel
    {
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Current password")]
        public string OldPassword { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "New password")]
        public string NewPassword { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm new password")]
        [Compare("NewPassword", ErrorMessage = 
            "The new password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
    }
  
  
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
  
  
    public class RegisterViewModel
    {
        [Required]
        [Display(Name = "User name")]
        public string UserName { get; set; }
  
        [Required]
        [StringLength(100, ErrorMessage = 
            "The {0} must be at least {2} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "Password")]
        public string Password { get; set; }
  
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = 
            "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
  
        // New Fields added to extend Application User class:
  
        [Required]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
  
        [Required]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
  
        [Required]
        public string Email { get; set; }
  
        // Return a pre-poulated instance of AppliationUser:
        public ApplicationUser GetUser()
        {
            var user = new ApplicationUser()
            {
                UserName = this.UserName,
                FirstName = this.FirstName,
                LastName = this.LastName,
                Email = this.Email,
            };
            return user;
        }
    }
  
  
    public class EditUserViewModel
    {
        public EditUserViewModel() { }
  
        // Allow Initialization with an instance of ApplicationUser:
        public EditUserViewModel(ApplicationUser user)
        {
            this.UserName = user.UserName;
            this.FirstName = user.FirstName;
            this.LastName = user.LastName;
            this.Email = user.Email;
        }
  
        [Required]
        [Display(Name = "User Name")]
        public string UserName { get; set; }
  
        [Required]
        [Display(Name = "First Name")]
        public string FirstName { get; set; }
  
        [Required]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
  
        [Required]
        public string Email { get; set; }
    }
  
  
    public class SelectUserRolesViewModel
    {
        public SelectUserRolesViewModel() 
        {
            this.Roles = new List<SelectRoleEditorViewModel>();
        }
  
  
        // Enable initialization with an instance of ApplicationUser:
        public SelectUserRolesViewModel(ApplicationUser user) : this()
        {
            this.UserName = user.UserName;
            this.FirstName = user.FirstName;
            this.LastName = user.LastName;
  
            var Db = new ApplicationDbContext();
  
            // Add all available roles to the list of EditorViewModels:
            var allRoles = Db.Roles;
            foreach(var role in allRoles)
            {
                // An EditorViewModel will be used by Editor Template:
                var rvm = new SelectRoleEditorViewModel(role);
                this.Roles.Add(rvm);
            }
  
            // Set the Selected property to true for those roles for 
            // which the current user is a member:
            foreach(var userRole in user.Roles)
            {
                var checkUserRole = 
                    this.Roles.Find(r => r.RoleName == userRole.Role.Name);
                checkUserRole.Selected = true;
            }
        }
  
        public string UserName { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public List<SelectRoleEditorViewModel> Roles { get; set; }
    }
  
    // Used to display a single role with a checkbox, within a list structure:
    public class SelectRoleEditorViewModel
    {
        public SelectRoleEditorViewModel() {}
        public SelectRoleEditorViewModel(IdentityRole role)
        {
            this.RoleName = role.Name;
        }
  
        public bool Selected { get; set; }
  
        [Required]
        public string RoleName { get; set;}
    }
}

Extending the Account Controller

Now that we have our Models and ViewModels mostly in place, let's look at how it all comes together in the Controller. Our current AccountController defines Controller Actions for the following:

  • Register (Essentially creates a new user)
  • Manage (Essentially allows the user to change their password)
  • Login
  • LogOff

Also, the above methods are focused upon allowing anonymous users to self-register, and create their own user account.

We are not planning to allow self-registration, and our requirements establish that user accounts are to be created by an administrator. Also, we have extended our ApplicationUser model to include some additional properties. From a functional perspective, we would like to se the following behavior implemented:

  • View a list of user accounts (Index), with links to various relevant functionality (Edit, Roles, Etc.)
  • Create a new User (We will co-opt the "Register" method for this, but we will extend it significantly).
  • Edit a User (Administrators need to be able to edit user accounts, assign roles, and such)
  • Delete a User (We want to be able to remove User accounts (or at least render them active or inactive)
  • Assign Roles to a User
  • Login
  • Log Off

Before we proceed, understand that there are countless possible major and minor variations we could consider for the above. I chose an application model which was simple, and for our purposes, rather arbitrary. For example, it could be your application requirements allow for self-registration of anonymous users into a default Role of some sort. The model I represent here is by purpose rather limited in scope, as we are trying to see concepts without getting too distracted by a complex implementation. I leave to you to expand from here into more complex (and more useful to you) application models.

In keeping with the above, I have added the [Authorize(Roles = "Admin")] attribute to all of the administrative methods, with the assumption that our administrative role will be called (wait for it . . .) "Admin." More on this later too.

Where was I?

Oh, yeah. So, looking at the functional needs in the list above, I am going to modify my AccountController to incorporate the above items. As mentioned parenthetically, I am simply going to co-opt the Register controller method and use it for what should probably be named Create (out of laziness at this point!).

Modifying The AccountController Register Method (Create User)

First, we will look at modifying our existing Register method(s) to accommodate our new ApplicationUser properties. We want to be able to create a new ApplicationUser in our View, and then persist the new record in our database.

Modified Register Method:
C#
[Authorize(Roles = "Admin")]
public ActionResult Register()
{
    return View();
}
  
  
[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = model.GetUser();
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            return RedirectToAction("Index", "Account");
        }
    }
  
    // If we got this far, something failed, redisplay form
    return View(model);
}

In the above, we have not changed much except using the handy GetUser() method defined on our RegisterViewModel to retrieve an instance of ApplicationUser, populated and ready to persist in our database. Also, we are redirecting to a new Index method we will define momentarily on our AccountController.

Adding The AccountController Index Method (View List of Users)

Previously, the AccountController did not have an Index method. We need a way for our Administrators to view a list of users of our application, and access the functionality to edit, assign roles, and delete. Once again, accessing the user account data is an admin function, so we have added the [Authorize] attribute as well.

The New Index Method:
C#
[Authorize(Roles = "Admin")]
public ActionResult Index()
{
    var Db = new ApplicationDbContext();
    var users = Db.Users;
    var model = new List<EditUserViewModel>();
    foreach(var user in users)
    {
        var u = new EditUserViewModel(user);
        model.Add(u);
    }
    return View(model);
}

Our Index method uses a List<EditUserViewModel> for now, as it contains all the information needed for display in our list. Contrary to what I said above, I have re-used a ViewModel here. I should probably fix that, but you can make your own decision on this point.

Notice that instead of performing the tedious mapping of properties from ApplicationUser instance to each EditUserViewModel within this method, I simply pass the ApplicationUser instance into the overridden constructor on EditUserViewModel. Our Index.cshtml View will expect a List<EditUserViewModel> as the model for display.

Adding The AccountController Edit Method

We have added an Edit method to facilitate administrative updating of User account data. There are some details to pay attention to in the Edit method implementation, at least in my version. First, while the method still accepts a parameter named id, what we will actually be passing to the method when a request is routed here will be a UserName. Why? I decided to follow the lead of the ASP.NET team on this. They are not passing User Id's (which are GUID's) out into the public HTML, so neither will I.

Also, by design and constraint, the UserName is unique in our database, and will already be a semi-public piece of information. Just something to keep in mind – the id parameter for public Account Action methods will be the User (or, as the case may be, Role) name, and not an integer. Lastly, I didn't want to add a whole new route just to rename a single route parameter which is serving essentially the same purpose as an int id.

That said, the following is the new Edit method, which will be used when an Administrator wishes to update user information:

The New Edit Method:
C#
[Authorize(Roles = "Admin")]
public ActionResult Edit(string id, ManageMessageId? Message = null)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new EditUserViewModel(user);
    ViewBag.MessageId = Message;
    return View(model);
}


[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(EditUserViewModel model)
{
    if (ModelState.IsValid)
    {
        var Db = new ApplicationDbContext();
        var user = Db.Users.First(u => u.UserName == model.UserName);

        // Update the user data:
        user.FirstName = model.FirstName;
        user.LastName = model.LastName;
        user.Email = model.Email;
        Db.Entry(user).State = System.Data.Entity.EntityState.Modified;
        await Db.SaveChangesAsync();
        return RedirectToAction("Index");
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

In the above, we use Linq to grab a reference to the specific user based on the UserName passed in as the id parameter in the first (GET) Edit method. We then populate an instance of EditUserViewModel by passing the ApplicationUser Instance to the constructor, and pass the ViewModel on to the Edit.cshtml View.

When the View returns our updated model to the second (POST) Edit method, we do much the same in reverse. We retrieve the User record from the database, then update with the model data, and save changes.

In our View, it will be important to remember that we cannot allow editing of the UserName property itself (at least, not under our current model, which considers the UserName to be an inviolate identifier).

Adding the AccountController Delete Method

We are adding a Delete (GET) method and a DeleteConfirmed (POST) method to the AccountController class. In my implementation, this method will actually delete the selected user from the database. You may decide instead to flag the database record as deleted, or implement some other method of managing unwanted user records.

You might also forego a delete method, and instead add a Boolean Inactive property to the ApplicationUser class, and manage Active/Inactive status through the Edit method discussed previously. Again, there are many permutations to the design model here. I went with the simplest for the sake of clarity.

The Delete method implementation here is straightforward so long as we remember, once again, that the id parameter passed to each of the two related methods below is actually a UserName.

The New Delete Methods:
C#
[Authorize(Roles = "Admin")]
public ActionResult Delete(string id = null)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new EditUserViewModel(user);
    if (user == null)
    {
        return HttpNotFound();
    }
    return View(model);
}
  
  
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
[Authorize(Roles = "Admin")]
public ActionResult DeleteConfirmed(string id)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    Db.Users.Remove(user);
    Db.SaveChanges();
    return RedirectToAction("Index");
}

As we can see, the DeleteConfirmed method is decorated with a HttpPost attribute, and an ActionName attribute "Delete" which means POST requests routed to Delete will be routed here. Both methods use the UserName passed as the id parameter to look up the appropriate ApplicationUser in the database.

Once again, in contrast to what I said earlier, I re-used the EditUserViewModel to pass to the Delete.cshtml View.

Adding the UserRoles Method to the AccountController

Lastly, we add the UserRoles method pair. This is where we manage assignment of user accounts to various application roles.

The implementation here looks relatively simple, and pretty similar to the other controller methods we have examined so far. However, under the hood in the SelectUserRolesViewModel and in the IdentityManager class, there is a lot going on. First, the code:

The New UserRoles Method(s):
C#
[Authorize(Roles = "Admin")]
public ActionResult UserRoles(string id)
{
    var Db = new ApplicationDbContext();
    var user = Db.Users.First(u => u.UserName == id);
    var model = new SelectUserRolesViewModel(user);
    return View(model);
}
  
  
[HttpPost]
[Authorize(Roles = "Admin")]
[ValidateAntiForgeryToken]
public ActionResult UserRoles(SelectUserRolesViewModel model)
{
    if(ModelState.IsValid)
    {
        var idManager = new IdentityManager();
        var Db = new ApplicationDbContext();
        var user = Db.Users.First(u => u.UserName == model.UserName);
        idManager.ClearUserRoles(user.Id);
        foreach (var role in model.Roles)
        {
            if (role.Selected)
            {
                idManager.AddUserToRole(user.Id, role.RoleName);
            }
        }
        return RedirectToAction("index");
    }
    return View();
}

As we can see above, and incoming GET request routed to the UserRoles method is handled similarly to those in previous methods. The UserName passed as the id parameter is used to retrieve the User record from the database, and then an instance of SelectUserRolesViewModel is initialized, passing the ApplicationUser instance to the constructor.

Here is where things get interesting. Let's take another look at the code for our SelectUserRolesViewModel from the AccountViewModels.cs file:

Code for SelectUserRolesViewModel – Revisited:
C#
public class SelectUserRolesViewModel
{
    public SelectUserRolesViewModel() 
    {
        this.Roles = new List<SelectRoleEditorViewModel>();
    }
  
  
    // Enable initialization with an instance of ApplicationUser:
    public SelectUserRolesViewModel(ApplicationUser user) : this()
    {
        this.UserName = user.UserName;
        this.FirstName = user.FirstName;
        this.LastName = user.LastName;
  
        var Db = new ApplicationDbContext();
  
        // Add all available roles to the list of EditorViewModels:
        var allRoles = Db.Roles;
        foreach(var role in allRoles)
        {
            // An EditorViewModel will be used by Editor Template:
            var rvm = new SelectRoleEditorViewModel(role);
            this.Roles.Add(rvm);
        }
  
        // Set the Selected property to true for those roles for 
        // which the current user is a member:
        foreach(var userRole in user.Roles)
        {
            var checkUserRole = 
                this.Roles.Find(r => r.RoleName == userRole.Role.Name);
            checkUserRole.Selected = true;
        }
    }
  
    public string UserName { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<SelectRoleEditorViewModel> Roles { get; set; }
}

During initialization, we are populating a List<SelectRoleEditorViewModel>() with all the roles available in the application. First, this is a prime candidate for refactoring, as I am performing data access within the object constructor (a general no-no). Second, SelectRoleEditorViewModel? What?

In my current implementation, we will see that the SelectUserRolesViewModel is passed to the UserRoles.cshtml view. We want to display the basic user details (so we know which user we are assigning roles to – always important to know), as well as a list of all available Roles. I have decided to facilitate role assignment using checkboxes, whereby roles are assigned to the user by checking one or more (or none!) checkboxes.

This is where the EditorViewModel comes in. We are going to use a common technique for adding checkboxes to a table and allowing the user to select from the list of items.

Let's revisit the code for SelectRoleEditorViewModel, which we defined in our AccountViewModels.cs file.

The SelectRoleEditorViewModel represents an individual role, and as we can see from the following code, includes a Boolean field used to indicate the Selected status for that role:

Code for SelectRoleEditorViewModel, Revisited:
C#
public class SelectRoleEditorViewModel
{
    public SelectRoleEditorViewModel() { }
    public SelectRoleEditorViewModel(IdentityRole role)
    {
        this.RoleName = role.Name;
    }
  
    public bool Selected { get; set; }
  
    [Required]
    public string RoleName { get; set; }
}

This EditorViewModel will be used by a special View called an EditorTemplate, which we will look at shortly. For now, bear in mind that the SelectUserRolesViewModel contains a List of SelectRoleEditorViewModel objects (yes, the naming of these could be better and posed some challenges – I am open to suggestion here! For the moment, I try to think of them as "SelectUserRoles-ViewModel" and "SelectRole-EditorViewModel" if that helps).

That covers the modified or new items in our AccountController. Now let's look at our Views.

Basic Views for Role-Based Identity Management

We already have a few of the views we will need, we just need to modify them a bit. In addition, we need to add a few new ones. We'll start by modifying our existing views to suit our needs, beginning with the Register.cshmtl View.

Modifying the Register.cshtml View File

The Register view as it currently sits was designed to allow user self-registration. As we have co-opted the Register method on the AccountController for restricted administrative use, so we will co-opt the Register.cshtml View for our purposes.

Essentially, all we need to do is add some additional HTML and Razor-Syntax code to the file to accommodate the new properties we added to our ApplicationUser class:

Modifying the Register.cshtml File:
C#
@model AspNetRoleBasedSecurity.Models.RegisterViewModel
@{
    ViewBag.Title = "Register";
}
  
<h2>@ViewBag.Title.</h2>
  
@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Create a new account.</h4>
    <hr />
    @Html.ValidationSummary()
    <div class="form-group">
        @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.Password, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
        </div>
    </div>
  
    // Add the LastName, FirstName, and Email Properties:
    <div class="form-group">
        @Html.LabelFor(m => m.LastName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.LastName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.FirstName, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.FirstName, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" class="btn btn-default" value="Register" />
        </div>
    </div>
}

Next, we will create our edit view. To do this, we can right-click on the Edit Action method declaration in AccountController and let VS do the work for us:

Create View for the Edit Method:

add-view-edit-method

We next see the Add View Dialog. Choose the Edit template from the Template drop-down, and select the EditUserViewModel class from the Model Class drop-down. We already have a data context, so leave that blank.

Add View Dialog:

add-view-dialog

Repeat the process above for the Delete and Index methods. Choose the appropriate template for each (use the List template for the Index View, as we want to display a list of User Accounts), and use EditUserViewModel as the Model Class for both.

Fine-Tuning the Index View

We need to make a few minor changes to our index view.

Notice near the bottom, where the template has provided handy links for Edit, Details, and Delete. We will change the "Details" link to instead point to our UserRoles Action method. Also, we need to replace the commented out route parameters such that the Username is passed as the id Parameter.

Lastly, up near the top of the file the is some razor code for an Action link to create a new user. Replace the Action method parameter "Create" with our co-opted "Register" method.

After our modifications, the final Index.cshtml file should look like this:

Modified Index.cshtml File:
C#
@model IEnumerable<AspNetRoleBasedSecurity.Models.EditUserViewModel>
  
@{
    ViewBag.Title = "Index";
}
  
<h2>Index</h2>
  
<p>
    @Html.ActionLink("Create New", "Register") 
</p>
<table class="table">
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.UserName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.FirstName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.LastName)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Email)
        </th>
        <th></th>
    </tr>
  
@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.UserName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.FirstName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.LastName)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Email)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id = item.UserName }) |
            @Html.ActionLink("Roles", "UserRoles", new { id = item.UserName }) |
            @Html.ActionLink("Delete", "Delete", new { id = item.UserName })
        </td>
    </tr>
}
  
</table>

Now we can get back to that whole User Roles issue.

Create the UserRoles.cshtml View

We can use the VS Add View method to create our UserRoles.cshtml View as we did with the previous views. However, we will be doing most of our work from scratch on this one. Right-Click on the UserRoles method of AccountController and select Add View. This time, however, choose the Empty template option from the Template drop-down, and choose SelectUserRolesViewModel from the Model Class drop-down.

You should now have a mostly empty UserRoles.cshtml file the looks like this:

The Empty UserRoles.cshtml File:
C#
@model AspNetRoleBasedSecurity.Models.SelectUserRolesViewModel
  
@{
    ViewBag.Title = "UserRoles";
}
  
<h2>UserRoles</h2>

From here, we will add our code manually. We want to display the basic user information, followed by a list of the available roles to which the user can be assigned (or removed). We want the list of roles to feature checkboxes as the selection mechanism.

To accomplish the above, we will add our Razor code as follows:

Added Code to the UserRoles.cshtml File:
C#
@model AspNetRoleBasedSecurity.Models.SelectUserRolesViewModel
  
@{
    ViewBag.Title = "User Roles";
}
  
<h2>Roles for user @Html.DisplayFor(model => model.UserName)</h2>
<hr />
  
@using (Html.BeginForm("UserRoles", "Account", FormMethod.Post, new { encType = "multipart/form-data", name = "myform" }))
{
    @Html.AntiForgeryToken()
  
    <div class="form-horizontal">
        @Html.ValidationSummary(true)
        <div class="form-group">
            <div class="col-md-10">
                @Html.HiddenFor(model => model.UserName)
            </div>
        </div>
  
        <h4>Select Role Assignments</h4>
        <br />
        <hr />
  
        <table>
            <tr>
                <th>
                    Select
                </th>
                <th>
                    Role
                </th>
            </tr>
        @Html.EditorFor(model => model.Roles)
        </table>
        <br />
        <hr />
  
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-default" />
            </div>
        </div>
    </div>
}

In the above, we have set up a display header featuring the UserName, and created an HTML Table with header elements for Select and Role. The Select column will contain the checkboxes for each role, and the Role column will, obviously, display the Role names. Below the table header set up, notice the line:

C#
@Html.EditorFor(model => model.Roles)

This is where we return to that EditorTemplate concept. An Editor template is a Shared View, and needs to be located in the Views => Shared => EditorTemplates folder in your project. You may need to create the folder yourself. Create the SelectRoleEditorViewModel Editor Template by right-clicking on your new EditorTemplates folder and selecting Add View. Use the Empty template again, and name the view SelectRoleEditorViewModel (this is important). Choose SelectRoleEditorViewModel from the Model Classes drop-down. When you are done you should have a .cshtml file that looks like this:

The Empty SelectRoleEditorViewModel Editor Template File:
C#
@model AspNetRoleBasedSecurity.Models.SelectRoleEditorViewModel
  
@{
    ViewBag.Title = "SelectRoleEditorViewModel";
}
  
<h2>SelectRoleEditorViewModel</h2>

From here, we will add a few lines, so that our file looks like this:

Modified SelectRoleEditorViewModel Editor Template File:
C#
@model AspNetRoleBasedSecurity.Models.SelectRoleEditorViewModel
@Html.HiddenFor(model => model.RoleName)
<tr>
    <td style="text-align:center">
        @Html.CheckBoxFor(model => model.Selected)
    </td>
    <td>
        @Html.DisplayFor(model => model.RoleName)
    </td>
</tr>

Now we have an editor template for our SelectRoleEditorViewModel. The code in our UserRoles.cshtml View will use this template to render our list of roles, including the checkboxes. Selections made in the checkboxes will be reflected in our model and returned, with the Role name, to the controller when the form is submitted.

Adding the Admin Tab to the Main Site Page

We are almost there. However, none of these new views and functionality do us much good if we can't get to it from within our application. We need to add an Admin tab to our main site page, and also remove the ability for anonymous users to access the registration link included on the site by default. To do this, we need to modify the _Layout.cshtml file in the Views => Shared folder.

Modified _Layout.cshtml File
C#
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
        <li>@Html.ActionLink("Admin", "Index", "Account")</li>
    </ul>
    @Html.Partial("_LoginPartial")
</div>

The code above is from just about the middle of the _Layout.cshtml View file. Add the "Admin" ActionLink to create a tab link pointing to the Index method of our AccountController.

Remove the Register Link from the _LoginPartial.cshtml View

Last, we want to remove the link to the Register method from the main site layout. This link is found on the _LoginPartial.cshtml file, again located in the Views => Shared folder. At the bottom of this file, remove the "Register" Action Link:

Remove the Register Link from _LoginPartial.cshtml:
C#
else
{
    <ul class="nav navbar-nav navbar-right">
        <li>@Html.ActionLink("Register", "Register", "Account", routeValues: null, htmlAttributes: new { id = "registerLink" })</li>
        <li>@Html.ActionLink("Log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })</li>
    </ul>
}

Now all our views should be ready.

Set Up and Run Entity Framework Migrations

Now that we have most of the pieces in place, it's time to set up Code-First Migrations with Entity Framework. Also, because we are ostensibly building an application in which only users in an administrative role can create or edit users, we need to seed our application with an initial Admin user. Further, since we don't plan to allow creation or modification of roles, we need to seed the database with the roles we expect to use in our application, since we can't create them from within the application itself. We covered EF Code-First Migrations fairly thoroughly in the previous article, so I am going to skim through it this time. First, enable migrations by typing the following in the Package Manager Console:

Enable EF Migrations in Your Project:
C#
PM> Enable-Migrations –EnableAutomaticMigrations

Now, open the Migrations => Configuration.cs file and add the following code (tune it up to suit your specifics. You probably don't want to add MY information as your admin user. Also note, whatever password you provide to start with must conform to the constraints of the application, which appears to require at least one capital letter, and at least one number:

Modify the EF Migrations Configuration File with Seed Data:
C#
using AspNetRoleBasedSecurity.Models;
using System.Data.Entity.Migrations;

namespace AspNetRoleBasedSecurity.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
  
    internal sealed class Configuration : DbMigrationsConfiguration<ApplicationDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }
  
  
        protected override void Seed(ApplicationDbContext context)
        {
            this.AddUserAndRoles();
        }
  
  
        bool AddUserAndRoles()
        {
            bool success = false;
  
            var idManager = new IdentityManager();
            success = idManager.CreateRole("Admin");
            if (!success == true) return success;
  
            success = idManager.CreateRole("CanEdit");
            if (!success == true) return success;
  
            success = idManager.CreateRole("User");
            if (!success) return success;
  
  
            var newUser = new ApplicationUser()
            {
                UserName = "jatten",
                FirstName = "John",
                LastName = "Atten",
                Email = "jatten@typecastexception.com"
            };
  
            // Be careful here - you  will need to use a password which will 
            // be valid under the password rules for the application, 
            // or the process will abort:
            success = idManager.CreateUser(newUser, "Password1");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "Admin");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "CanEdit");
            if (!success) return success;
  
            success = idManager.AddUserToRole(newUser.Id, "User");
            if (!success) return success;
  
            return success;
        }
    }
}

Once that is done, run the following command from the Package Manager Console:

Add the Initial EF Migration
C#
Add-Migration Init

Then create the database by running the Update-Database command:

Update Database Command:
C#
Update-Database

If all went well, your database should be created as a SQL Server (local) database in the App_Data folder. If you want to point to a different Database Server, review the previous article where we discuss pointing the default connection string to a different server. You can check to see if your database was created properly by opening the Server Explorer window in Visual Studio. Or, of course, you could simply run your application, and see what happens!

Use [Authorize] Attribute to Control Access

From this point, we can regulate access to different application functionality using the [Authorize] Attribute. We have already seen examples of this on the methods in our AccountController, where access to everything except the Login method is restricted to users of the Admin role.

Use [Authorize] Attribute to Control Access to Functionality:
C#
[AllowAnonymous]
public MyPublicMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin, CanEdit, User")]
public MyPrettyAccessibleMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin, CanEdit")]
public MyMoreRestrictiveMethod()
{
    // Code
}
  
  
[Authorize(Role = "Admin")]
public MyVeryRestrictedMethod()
{
    // Code
}

In the code above, we see progressively more restricted method access based on Role access defined using the [Authorize] attribute. At the moment, our role definitions are not "tiered" in a manner by which a higher-level role inherits the permissions associated with a more restricted role. For example, if a method is decorated with an [Authorize] attribute granting access to members of the User Role, it is important to note that ONLY members of that role will be able to access the method. Role access must be explicitly and specifically granted for each role under this scenario.

Role Permissions are not Inherited:
C#
// Admins can't access this method:
[Authorize(Role = "Users")]
public SomeMethod()
{
    // Code
}

// Admins AND Users can access this method:
[Authorize(Role = "Users, Admins")]
public SomeMethod()
{
    // Code
}

Contrary to our experience with most operating system security, members of the Admin role do not automatically get all the permissions of the User role. We could probably achieve this, but such is beyond the scope of this article.

A Note About Roles and Role Naming

For our purposes here, I have used some rather generic role names, since we really don't have any business cases to consider when using roles to manage application access. The ASP.NET team recommends (and I agree) the it is best to use descriptive and limiting role definitions which describe, to an extent, the permissions associated with that role. For example, instead of a generic "Admin" role, one might create an "IdentityManager" role specific to Account and Identity management, and other such descriptive role names as make sense in the business context of your application.

Wrapping It Up

In this article we have created a very simple implementation of Role-Based Identity Management. As I mentioned at the beginning, the model used here, outside of any business context, is a little basic. I have attempted to create show some of the basics involved with using the ASP.NET MVC Identity system, extending it to include some custom properties, and modifying the use to suit a basic business case. There is a lot to know about Web Application security, and in my mind, it is not the place to re-invent any wheels. In the application discussed here, we have re-jiggered the components of the ASP.NET Identity model, but we have used the core pieces as designed instead of inventing our own authorization mechanism. Hopefully, this rather long article has been helpful, and I have not propagated any bad information. Please do let me know, either in the comments, or by email if you find any significant issues here. I will correct them promptly.

Additional Resources and Items of Interest

Identity v2.0:
Identity 1.0 and Other Items:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer XIV Solutions
United States United States
My name is John Atten, and my username on many of my online accounts is xivSolutions. I am Fascinated by all things technology and software development. I work mostly with C#, Javascript/Node.js, Various flavors of databases, and anything else I find interesting. I am always looking for new information, and value your feedback (especially where I got something wrong!)

Comments and Discussions

 
AnswerRe: [My vote of 1] Very poor sample app Pin
Nigel Stratton21-Feb-14 14:46
Nigel Stratton21-Feb-14 14:46 
AnswerRe: [My vote of 1] Very poor sample app Pin
Gizz2-Apr-14 20:53
Gizz2-Apr-14 20:53 
QuestionNice! Pin
Volynsky Alex29-Dec-13 12:46
professionalVolynsky Alex29-Dec-13 12:46 
QuestionEntityType 'EditUserViewModel' has no key defined Pin
qvisionsa27-Dec-13 14:12
qvisionsa27-Dec-13 14:12 
AnswerRe: EntityType 'EditUserViewModel' has no key defined Pin
John Atten28-Dec-13 1:39
John Atten28-Dec-13 1:39 
GeneralRe: EntityType 'EditUserViewModel' has no key defined Pin
qvisionsa28-Dec-13 7:52
qvisionsa28-Dec-13 7:52 
QuestionRe: EntityType 'EditUserViewModel' has no key defined Pin
Iftikhar Ali27-Jan-14 5:44
Iftikhar Ali27-Jan-14 5:44 
AnswerRe: EntityType 'EditUserViewModel' has no key defined Pin
qvisionsa27-Jan-14 10:36
qvisionsa27-Jan-14 10:36 
Review the article and find that when you are creating the view, you must not select a model. Leave it empty. The article is wright.
GeneralRe: EntityType 'EditUserViewModel' has no key defined Pin
Iftikhar Ali28-Jan-14 4:08
Iftikhar Ali28-Jan-14 4:08 
GeneralRe: EntityType 'EditUserViewModel' has no key defined Pin
John Atten28-Jan-14 4:28
John Atten28-Jan-14 4:28 
GeneralRe: EntityType 'EditUserViewModel' has no key defined Pin
Iftikhar Ali28-Jan-14 4:38
Iftikhar Ali28-Jan-14 4:38 
Questionadmin user is not being created Pin
Frank Silva10-Dec-13 15:58
Frank Silva10-Dec-13 15:58 
AnswerRe: admin user is not being created Pin
John Atten10-Dec-13 18:28
John Atten10-Dec-13 18:28 
GeneralRe: admin user is not being created Pin
Frank Silva11-Dec-13 11:23
Frank Silva11-Dec-13 11:23 
AnswerRe: admin user is not being created Pin
Christopher Stout12-Dec-13 12:16
Christopher Stout12-Dec-13 12:16 
GeneralRe: admin user is not being created Pin
Robert J. Bullock14-Jan-14 10:26
Robert J. Bullock14-Jan-14 10:26 
GeneralRe: admin user is not being created Pin
John Atten14-Jan-14 10:32
John Atten14-Jan-14 10:32 
GeneralRe: admin user is not being created Pin
Robert J. Bullock14-Jan-14 10:39
Robert J. Bullock14-Jan-14 10:39 
GeneralRe: admin user is not being created Pin
Robert J. Bullock14-Jan-14 11:03
Robert J. Bullock14-Jan-14 11:03 
GeneralRe: admin user is not being created Pin
John Atten14-Jan-14 11:23
John Atten14-Jan-14 11:23 
QuestionMy ApplicationDbContext.Users Object does not expose a First Method, and a bonus Pin
Frank Silva10-Dec-13 12:26
Frank Silva10-Dec-13 12:26 
AnswerRe: My ApplicationDbContext.Users Object does not expose a First Method, and a bonus Pin
John Atten10-Dec-13 13:22
John Atten10-Dec-13 13:22 
GeneralRe: My ApplicationDbContext.Users Object does not expose a First Method, and a bonus Pin
Frank Silva10-Dec-13 13:55
Frank Silva10-Dec-13 13:55 
QuestionCustom Role Group Pin
Zulfiddin2-Dec-13 22:50
Zulfiddin2-Dec-13 22:50 
AnswerRe: Custom Role Group Pin
John Atten3-Dec-13 1:27
John Atten3-Dec-13 1:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.