The .NET framework provides a provider model that allows developers to implement common user management and authentication functionality in ASP.NET applications. Out of the box, the framework ships with a few providers that allow you to easily wire up a user management system with very little to zero back end code. For instance, you can use the SqlMembershipProvider and the SqlRoleProvider which will create a database for you with tables for maintaining users and their roles. There are also providers available that use Active Directory as the user and role data store.
This is great in many cases and can save you from writing a lot of tedious plumbing code. However, there are many instances where this may not be appropriate:
- I personally don’t feel comfortable having third party code create my user schema. Perhaps I’m being overly sensitive here.
- You don’t use Sql Server or Active Directory. If you are using MySql to manage your user data, neither the out of the box providers will work for you.
- You already have your own user data and schema which you want to continue using. You will need to implement your own providers to work with your schema.
I fall into the first and third scenarios listed above. I have multiple applications that all run on top of a common user/role management database. I want to be able to use Forms Authentication to provide my web security and I want all user validation and role checking to leverage the database schema that I already have. Fortunately, creating custom Membership and Role providers proved to be rather easy and this post will walk you through the necessary steps to get up and running.
Defining Users, Roles and Rights
My schema has the following key tables:
A users table which defines individual users and their user names, human names, passwords and role.
- A role table that defines individual roles. It’s a simple lookup table with role_id and role_name columns.
- A right table which is also a simple lookup table with Right_id and right_name columns.
- A role_right table which defines many to many relationships between roles and rights. It has a role_id and right_id columns. Individual roles contain a collection of one to N number of rights.
These tables are mapped to classes via nHibernate.
Here is the User class:
public class User : IPrincipal { protected User() { } public User(int userId, string userName, string fullName, string password) { UserId = userId; UserName = userName; FullName = fullName; Password = password; } public virtual int UserId { get; set; } public virtual string UserName { get; set; } public virtual string FullName { get; set; } public virtual string Password { get; set; } public virtual IIdentity Identity { get; set; } public virtual bool IsInRole(string role) { if (Role.Description.ToLower() == role.ToLower()) return true; foreach (Right right in Role.Rights) { if (right.Description.ToLower() == role.ToLower()) return true; } return false; } }
You will notice that User derives from IPrincipal. This is not necessary to allow User to operate with my MembershipProvider implementation, but it allows me the option to use the User class within other frameworks that work with IPrincipal. For example, I may want to be able to directly interact with User when calling the Controller base class User property. This just requires me to implement the Identity property and the bool IsInRole(string roleName) method. When we get to the RoleProvider implementation, you will see that the IsInRole implementation is used there. You may also notice something else that may appear peculiar: a role here can be either a role or a right. This might be a bit of a hack to shim my finer grained right based schema into the ASP.NET role based framework, but it works for me.
Here are the Role and Right classes. They are simple data objects:
public class Role { protected Role() { } public Role(int roleid, string roleDescription) { RoleId = roleid; Description = roleDescription; } public virtual int RoleId { get; set; } public virtual string Description { get; set; } public virtual IList<Right> Rights { get; set; } } public class Right { protected Right() { } public Right(int rightId, string description) { RightId = rightId; Description = description; } public virtual int RightId { get; set; } public virtual string Description { get; set; } }
Implementing the providers
So now with the basic data classes behind us, we can implement the membership and role providers. These implementations must derrive from MembershipProvider and RoleProvider. Note that these base classes contain a lot of methods that I have no use for. The Membership Provider model was designed to handle all sorts of user related functionality like creating users, modifying paswords, etc. I just need basic logon and role checking; so a lot of my methods throw NotImplementedExceptions. However, all methods required to handle authentication and role checking are implemented.
Here is the Membership Provider:
public class AdminMemberProvider : MembershipProvider { public override string ApplicationName { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } public override bool ChangePassword(string username, string oldPassword, string newPassword) { throw new NotImplementedException(); } public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { throw new NotImplementedException(); } public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { throw new NotImplementedException(); } public override bool DeleteUser(string username, bool deleteAllRelatedData) { throw new NotImplementedException(); } public override bool EnablePasswordReset { get { throw new NotImplementedException(); } } public override bool EnablePasswordRetrieval { get { throw new NotImplementedException(); } } public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotImplementedException(); } public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { throw new NotImplementedException(); } public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { throw new NotImplementedException(); } public override int GetNumberOfUsersOnline() { throw new NotImplementedException(); } public override string GetPassword(string username, string answer) { throw new NotImplementedException(); } public override MembershipUser GetUser(string username, bool userIsOnline) { throw new NotImplementedException(); } public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { throw new NotImplementedException(); } public override string GetUserNameByEmail(string email) { throw new NotImplementedException(); } public override int MaxInvalidPasswordAttempts { get { throw new NotImplementedException(); } } public override int MinRequiredNonAlphanumericCharacters { get { throw new NotImplementedException(); } } public override int MinRequiredPasswordLength { get { throw new NotImplementedException(); } } public override int PasswordAttemptWindow { get { throw new NotImplementedException(); } } public override MembershipPasswordFormat PasswordFormat { get { throw new NotImplementedException(); } } public override string PasswordStrengthRegularExpression { get { throw new NotImplementedException(); } } public override bool RequiresQuestionAndAnswer { get { throw new NotImplementedException(); } } public override bool RequiresUniqueEmail { get { throw new NotImplementedException(); } } public override string ResetPassword(string username, string answer) { get { throw new NotImplementedException(); } } public override bool UnlockUser(string userName) { get { throw new NotImplementedException(); } } public override void UpdateUser(MembershipUser user) { get { throw new NotImplementedException(); } } IUserRepository _repository; public AdminMemberProvider() : this(null) { } public AdminMemberProvider(IUserRepository repository) : base() { _repository = repository ?? UserRepositoryFactory.GetRepository(); } public User User { get; private set; } public UserAdmin.DataEntities.User CreateUser(string fullName,string passWord, string email) { return (null); } public override bool ValidateUser(string username, string password) { if(string.IsNullOrEmpty(password.Trim())) return false; string hash = EncryptPassword(password); User user = _repository.GetByUserName(username); if (user == null) return false; if (user.Password == hash) { User = user; return true; } return false; } /// <summary> /// Procuses an MD5 hash string of the password /// </summary> /// <param name="password">password to hash</param> /// <returns>MD5 Hash string</returns> protected string EncryptPassword(string password) { //we use codepage 1252 because that is what sql server uses byte[] pwdBytes = Encoding.GetEncoding(1252).GetBytes(password); byte[] hashBytes = System.Security.Cryptography.MD5.Create().ComputeHash(pwdBytes); return Encoding.GetEncoding(1252).GetString(hashBytes); } }
The key method implemented here is ValidateUser. This uses the Repository pattern to query the user by name and then compares the password in the repository with the password passed to the method. My default repository is backed by nHibernate, but it could be plain ADO or a fake repository for testing purposes. You will need to implement your own UserRepositoryFactory based on your data store. Note that I am encrypting the passed password before the comparison. This is because our passwords are hashed in the database.
Here is the Role Provider:
public class AdminRoleProvider : RoleProvider { IUserRepository _repository; public AdminRoleProvider(): this(UserRepositoryFactory.GetRepository()) { } public AdminRoleProvider(IUserRepository repository) : base() { _repository = repository ?? UserRepositoryFactory.GetRepository(); } public override bool IsUserInRole(string username, string roleName) { User user = _repository.GetByUserName(username); if(user!=null) return user.IsInRole(roleName); else return false; } public override string ApplicationName { get { throw new NotImplementedException(); } set { throw new NotImplementedException(); } } public override void AddUsersToRoles(string[] usernames, string[] roleNames) { throw new NotImplementedException(); } public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames) { throw new NotImplementedException(); } public override void CreateRole(string roleName) { throw new NotImplementedException(); } public override bool DeleteRole(string roleName, bool throwOnPopulatedRole) { throw new NotImplementedException(); } public override bool RoleExists(string roleName) { throw new NotImplementedException(); } public override string[] GetRolesForUser(string username) { User user = _repository.GetByUserName(username); string[] roles = new string[user.Role.Rights.Count + 1]; roles[0] = user.Role.Description; int idx = 0; foreach (Right right in user.Role.Rights) roles[++idx] = right.Description; return roles; } public override string[] GetUsersInRole(string roleName) { throw new NotImplementedException(); } public override string[] FindUsersInRole(string roleName, string usernameToMatch) { throw new NotImplementedException(); } public override string[] GetAllRoles() { throw new NotImplementedException(); } }
Again, many methods of the base class are unimplemented because I did not need the functionality.
Wiring the providers in web.config
This is all the code we need for the “model” level user authentication and role checking. Next we have to wire these classes up in our web.config so that the application knows to use them. The following should be inside of <system.web>:
<authentication mode="Forms" > <forms loginUrl="~/LoginAccount/LogOn" path="/" /> </authentication> <authorization> <deny users="?"/> </authorization> <membership defaultProvider="AdminMemberProvider" userIsOnlineTimeWindow="15"> <providers> <clear/> <add name="AdminMemberProvider" type="UserAdmin.DomainEntities.AdminMemberProvider, UserAdmin" /> </providers> </membership> <roleManager defaultProvider="AdminRoleProvider" enabled="true" cacheRolesInCookie="true"> <providers> <clear/> <add name="AdminRoleProvider" type="UserAdmin.DomainEntities.AdminRoleProvider, UserAdmin" /> </providers> </roleManager>
Brining in the Controller and View
The only thing left now is to code the controllers. First our LoginAccountController needs to be able to log users in and out of the application:
Here is our login form in the view:
public class LoginAccountController : Controller { UserMemberProvider provider = (UserMemberProvider) Membership.Provider; public LoginAccountController() {} public ActionResult LogOn() { return View(); } [AcceptVerbs(HttpVerbs.Post)] public ActionResult LogOn(string userName, string password, string returnUrl) { if (!ValidateLogOn(userName, password)) return View(); UserAdmin.DataEntities.User user = provider.GetUser(); FormsAuthentication.SetAuthCookie(user.UserName, false); if (!String.IsNullOrEmpty(returnUrl) && returnUrl != "/") return Redirect(returnUrl); else return RedirectToAction("Index", "Home"); } public ActionResult LogOff() { FormsAuthentication.SignOut(); return RedirectToAction("Index", "Home"); } private bool ValidateLogOn(string userName, string password) { if (String.IsNullOrEmpty(userName)) { ModelState.AddModelError("username", "You must specify a username."); } if (String.IsNullOrEmpty(password)) { ModelState.AddModelError("password", "You must specify a password."); } if (!provider.ValidateUser(userName, password)) { ModelState.AddModelError("_FORM", "The username or password provided is incorrect."); } return ModelState.IsValid; } }
<div id="errorpanel"> <%= Html.ValidationSummary() %></div> <div class="signinbox"> <% using (Html.BeginForm()) { %> <table> <tr> <td>Email:</td> <td> <%= Html.TextBox("username", null, new { @class = "userbox"}) %> </td> </tr> <tr> <td>Password:</td> <td> <%= Html.Password("password", null, new { @class = "password"}) %> </td> </tr> <tr> <td> <input type="image" src="/Content/images/buttonLogin.gif" alt="Login" width="80" height="20" border="0" id="Image1" name="Image1"> </td> <td align="right" valign="bottom"></td> </tr> </table> <% } %> </div>
That’s it. It’s really pretty simple.