In 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.
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.
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
.
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):
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)
{
}
[AllowAnonymous]
public ActionResult Register()
{
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
}
public ActionResult Manage(ManageMessageId? message)
{
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Manage(ManageUserViewModel model)
{
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
AuthenticationManager.SignOut();
return RedirectToAction("Index", "Home");
}
protected override void Dispose(bool disposing)
{
}
#region Helpers
private IAuthenticationManager AuthenticationManager
{
}
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
}
private void AddErrors(IdentityResult result)
{
}
private bool HasPassword()
{
}
public enum ManageMessageId
{
}
private ActionResult RedirectToLocal(string returnUrl)
{
}
#endregion
}
}
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:
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.
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:
@{
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:
@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.
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:
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; }
}
}
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:
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
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.
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:
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:
using System.ComponentModel.DataAnnotations;
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; }
[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 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() { }
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>();
}
public SelectUserRolesViewModel(ApplicationUser user) : this()
{
this.UserName = user.UserName;
this.FirstName = user.FirstName;
this.LastName = user.LastName;
var Db = new ApplicationDbContext();
var allRoles = Db.Roles;
foreach(var role in allRoles)
{
var rvm = new SelectRoleEditorViewModel(role);
this.Roles.Add(rvm);
}
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; }
}
public class SelectRoleEditorViewModel
{
public SelectRoleEditorViewModel() {}
public SelectRoleEditorViewModel(IdentityRole role)
{
this.RoleName = role.Name;
}
public bool Selected { get; set; }
[Required]
public string RoleName { get; set;}
}
}
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!).
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:
[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");
}
}
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
.
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:
[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.
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:
[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);
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");
}
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).
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:
[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.
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):
[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:
public class SelectUserRolesViewModel
{
public SelectUserRolesViewModel()
{
this.Roles = new List<SelectRoleEditorViewModel>();
}
public SelectUserRolesViewModel(ApplicationUser user) : this()
{
this.UserName = user.UserName;
this.FirstName = user.FirstName;
this.LastName = user.LastName;
var Db = new ApplicationDbContext();
var allRoles = Db.Roles;
foreach(var role in allRoles)
{
var rvm = new SelectRoleEditorViewModel(role);
this.Roles.Add(rvm);
}
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:
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.
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.
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:
@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:
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:
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.
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:
@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.
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:
@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:
@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:
@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:
@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:
@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.
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
<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
.
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:
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.
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:
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:
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"
};
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
Add-Migration Init
Then create the database by running the Update-Database command:
Update Database Command:
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!
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:
[AllowAnonymous]
public MyPublicMethod()
{
}
[Authorize(Role = "Admin, CanEdit, User")]
public MyPrettyAccessibleMethod()
{
}
[Authorize(Role = "Admin, CanEdit")]
public MyMoreRestrictiveMethod()
{
}
[Authorize(Role = "Admin")]
public MyVeryRestrictedMethod()
{
}
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:
[Authorize(Role = "Users")]
public SomeMethod()
{
}
[Authorize(Role = "Users, Admins")]
public SomeMethod()
{
}
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.
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.
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.
Identity v2.0:
Identity 1.0 and Other Items:
John on GoogleCodeProject