May 9 2011

Implementing your own RoleProvider and MembershipProvider in MVC 3

Category: Intermediate | MVCBrian Legg @ 15:34

At some point you probably realized that users will need to log into your website. Identifying who your users are is known as authentication. You may also need the added ability to know what resources each user is allowed to access. This is accomplished in the form of roles using authorization. This functionality is built into the MVC framework right out of the box in the form of the Login control and the ASP.NET default membership and role providers. If you're like me you may want more control over what data is stored for a user and how it is stored. Towards this end we'll be implementing our own method of authorizing and authenticating users in this post. There are several different ways to do this depending on the level of control needed. We'll begin by doing the bare minimum and we'll add on more complexity and specialization as we go. Feel free to implement the parts you need and use the out-of-the-box features that do not need custom implementations. I'll try to keep these sections separate from one another to make following along easier.

For the purposes of this tutorial we'll use the following scenario: Our website supports a local bank. There will be two available roles for users: Member and Administrator. When storing customer information we will be interested in thier name, username, email, password and their role of course.

Setup

In order to follow along with this tutorial you'll need ASP.NET MVC 3 and Entity Framework 4.1. Both of these can be downloaded from Microsoft's website. An even easier way to accomplish this is to download and install ASP.NET MVC 3 which comes with NuGet (a Visual Studio package management plug-in). In order to load/install Entity Framework 4.1 using NuGet simply open Visual Studio 2010 and navigate to Tools -> Library Package Manager -> Package Manager Console. Once the console is open just type "install-package Entityframework". This will install Entity Framework 4.1 and add the reference to your open web application.

The starting point for this tutorial will be a new ASP.NET MVC 3 Web Application. I chose the Internet Application template which gives us an account controller and form authentication right away. I'll also be using Razor for the view engine.

Creating the User & Role Tables

When a user registers with your website we'll need a place to store this information. Since we'll no longer be using the out-of-the-box ASP.NET default role provider we'll need to create a table to contain this information ourselves. The schema and type of database you decide to use for these tables is completely up to you. That's the main reason for extending these providers to begin with, flexibility.

For this tutorial I'm creating a brand new SQL Server database under my project named LocalBankDB.mdf. However, you could use Oracle, SQLite, or any other database which can be connected to using ADO.NET. The process of creating these tables and thier relationship(s) is out of scope for this tutorial, but I'll be using the schema shown in this diagram:

 

As you can see from the diagram every user has 1 and only 1 role while a role can be assigned to any number of users. Besides this relationship we'll be storing the users name, user name, password (will be encrypted/hashed), and their email address.

Creating the User & Role Models

We'll be using EntityFramework 4.1 to create a model from our database. To begin right click on your Models folder and select Add -> New Item... Select ADO.NET Entity Data Model from the Data template and name our new model LocalBankModel.edmx then press Add. Since we have already defined our database we'll select the option to generate our model from our database.

 

Next you'll need to specify the database to use. In my case it's a local SQL Server database named LocalBankDB.mdf but you can use the New Connection option to connect to other types of databases if needed. Notice that VS has an option to create a connection string for us in the Web.Config file. This makes things much simpler so make sure this option is checked. I'll be naming the connection LocalBankEntities.

 

Select the User and Role tables and click Finish. This will create our LocalBankEntities OjbectContext which we can use to query the database and supports all CRUD operations. This will also create a Role and User EntityObject which encapsulates the structure of our database tables. A new entry will also be added to our Web.Config file, but we'll get to that in just a bit.

 

Creating a Repository (optional step)

I'm a big fan of the repository pattern which allows us to write friendly methods to encapsulate working with our entities. This allows our controllers to work with our custom classes instead of directly calling entity objects. This allows us to rebuild our database and our model layer without affecting our controller classes. To begin we'll create a new class called LocalBankRepository which contains a private instance of the LocalBankEntities object and has a number of methods for accessing basic CRUD scenarios.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;

namespace LocalBank.Models
{

    public class LocalBankRepository
    {

        #region Variables

        private LocalBankEntities entities = new LocalBankEntities();

        private const string MissingRole = "Role does not exist";
        private const string MissingUser = "User does not exist";
        private const string TooManyUser = "User already exists";
        private const string TooManyRole = "Role already exists";
        private const string AssignedRole = "Cannot delete a role with assigned users";

        #endregion

        #region Properties

        public int NumberOfUsers
        {
            get
            {
                return this.entities.Users.Count();
            }
        }

        public int NumberOfRoles
        {
            get
            {
                return this.entities.Roles.Count();
            }
        }

        #endregion

        #region Constructors

        public LocalBankRepository()
        {
            this.entities = new LocalBankEntities();
        }

        #endregion

        #region Query Methods

        public IQueryable<User> GetAllUsers()
        {
            return from user in entities.Users
                   orderby user.UserName
                   select user;
        }

        public User GetUser(int id)
        {
            return entities.Users.SingleOrDefault(user => user.ID == id);
        }

        public User GetUser(string userName)
        {
            return entities.Users.SingleOrDefault(user => user.UserName == userName);
        }

        public IQueryable<User> GetUsersForRole(string roleName)
        {
            return GetUsersForRole(GetRole(roleName));
        }

        public IQueryable<User> GetUsersForRole(int id)
        {
            return GetUsersForRole(GetRole(id));
        }

        public IQueryable<User> GetUsersForRole(Role role)
        {
            if (!RoleExists(role))
                throw new ArgumentException(MissingRole);

            return from user in entities.Users
                   where user.RoleID == role.ID
                   orderby user.UserName
                   select user;
        }

        public IQueryable<Role> GetAllRoles()
        {
            return from role in entities.Roles
                   orderby role.Name
                   select role;
        }

        public Role GetRole(int id)
        {
            return entities.Roles.SingleOrDefault(role => role.ID == id);
        }

        public Role GetRole(string name)
        {
            return entities.Roles.SingleOrDefault(role => role.Name == name);
        }

        public Role GetRoleForUser(string userName)
        {
            return GetRoleForUser(GetUser(userName));
        }

        public Role GetRoleForUser(int id)
        {
            return GetRoleForUser(GetUser(id));
        }

        public Role GetRoleForUser(User user)
        {
            if (!UserExists(user))
                throw new ArgumentException(MissingUser);

            return user.Role;
        }

        #endregion

        #region Insert/Delete

        private void AddUser(User user)
        {
            if (UserExists(user))
                throw new ArgumentException(TooManyUser);

            entities.Users.AddObject(user);
        }

        public void CreateUser(string username, string name, string password, string email, string roleName)
        {
            Role role = GetRole(roleName);

            if (string.IsNullOrEmpty(username.Trim()))
                throw new ArgumentException("The user name provided is invalid. Please check the value and try again.");
            if (string.IsNullOrEmpty(name.Trim()))
                throw new ArgumentException("The name provided is invalid. Please check the value and try again.");
            if (string.IsNullOrEmpty(password.Trim()))
                throw new ArgumentException("The password provided is invalid. Please enter a valid password value.");
            if (string.IsNullOrEmpty(email.Trim()))
                throw new ArgumentException("The e-mail address provided is invalid. Please check the value and try again.");
            if (!RoleExists(role))
                throw new ArgumentException("The role selected for this user does not exist! Contact an administrator!");
            if (this.entities.Users.Any(user => user.UserName == username))
                throw new ArgumentException("Username already exists. Please enter a different user name.");

            User newUser = new User()
            {
                UserName = username,
                Name = name,
                Password = FormsAuthentication.HashPasswordForStoringInConfigFile(password.Trim(), "md5"),
                Email = email,
                RoleID = role.ID
            };

            try
            {
                AddUser(newUser);
            }
            catch (ArgumentException ae)
            {
                throw ae;
            }
            catch (Exception e)
            {
                throw new ArgumentException("The authentication provider returned an error. Please verify your entry and try again. " +
                    "If the problem persists, please contact your system administrator.");
            }

            // Immediately persist the user data
            Save();
        }

        public void DeleteUser(User user)
        {
            if (!UserExists(user))
                throw new ArgumentException(MissingUser);

            entities.Users.DeleteObject(user);
        }

        public void DeleteUser(string userName)
        {
            DeleteUser(GetUser(userName));
        }

        public void AddRole(Role role)
        {
            if (RoleExists(role))
                throw new ArgumentException(TooManyRole);

            entities.Roles.AddObject(role);
        }

        public void AddRole(string roleName)
        {
            Role role = new Role()
            {
                Name = roleName
            };

            AddRole(role);
        }

        public void DeleteRole(Role role)
        {
            if (!RoleExists(role))
                throw new ArgumentException(MissingRole);

            if (GetUsersForRole(role).Count() > 0)
                throw new ArgumentException(AssignedRole);

            entities.Roles.DeleteObject(role);
        }

        public void DeleteRole(string roleName)
        {
            DeleteRole(GetRole(roleName));
        }

        #endregion

        #region Persistence

        public void Save()
        {
            entities.SaveChanges();
        }

        #endregion

        #region Helper Methods

        public bool UserExists(User user)
        {
            if (user == null)
                return false;

            return (entities.Users.SingleOrDefault(u => u.ID == user.ID || u.UserName == user.UserName) != null);
        }

        public bool RoleExists(Role role)
        {
            if (role == null)
                return false;

            return (entities.Roles.SingleOrDefault(r => r.ID == role.ID || r.Name == role.Name) != null);
        }

        #endregion

    }

}

There's quite a bit of code there but the 2 methods to pay attention to are the AddUser and CreateUser. I chose to keep AddUser private in order to force controllers to call my CreateUser method which validates the user, encrypts the users password, and persists the data to the database. There are many ways of accomplishing this so feel free to expand on or write your own repository for your user and role management.

Creating a Custom RoleProvider

Finally we get to the meat of this topic, creating our custom RoleProvider. This is actually quite simple. Create a new class called LocalBankRoleProvider which extends System.Web.Security.RoleProvider. Right-click on RoleProvider and choose to Implement Abstract Class.

 

Doing this will toss lots of method stubs into your class which throw a NotImplementedException. This is fine, we'll only be implementing 2 of these methods: IsUserInRole and GetRolesForUser. Also, make sure to add a local private variable to access our repository.

private LocalBankRepository repository;

public LocalBankRoleProvider()
{
    this.repository = new LocalBankRepository();
}

public override bool IsUserInRole(string userName, string roleName)
{
    User user = repository.GetUser(userName);
    Role role = repository.GetRole(roleName);

    if (!repository.UserExists(user))
        return false;
    if (!repository.RoleExists(role))
        return false;

    return user.Role.Name == role.Name;
}

public override string[] GetRolesForUser(string userName)
{
    Role role = this.repository.GetRoleForUser(userName);
    if (!this.repository.RoleExists(role))
        return new string[] { string.Empty };

    return new string[] { role.Name };
}

Creating a Custom MembershipProvider

The next step is to create our custom MembershipProvider. Create a new class LocalBankMembershipProvider which extends System.Web.Security.MembershipProvider. Just like the RoleProvider right-click on MembershipProvider and choose to Implement Abstract Class. Again you will get many methods which throw a NotImplementedException. We'll be implementing 2 of these methods and 1 property: ValidateUser, ChangePassword and MinRequiredPasswordLength.

private LocalBankRepository repository;

public override int MinRequiredPasswordLength
{
    get
    {
        return 6;
    }
}

public LocalBankMembershipProvider()
{
    this.repository = new LocalBankRepository();
}

public override bool ValidateUser(string username, string password)
{
    if (string.IsNullOrEmpty(password.Trim()) || string.IsNullOrEmpty(username.Trim()))
        return false;

    string hash = FormsAuthentication.HashPasswordForStoringInConfigFile(password.Trim(), "md5");

    return this.repository.GetAllUsers().Any(user => (user.UserName == username.Trim()) && (user.Password == hash));
}

public void CreateUser(string username, string fullName, string password, string email, string roleName)
{
    this.repository.CreateUser(username, fullName, password, email, roleName);
}

public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
    if (!ValidateUser(username, oldPassword) || string.IsNullOrEmpty(newPassword.Trim()))
        return false;

    User user = repository.GetUser(username);
    string hash = FormsAuthentication.HashPasswordForStoringInConfigFile(newPassword.Trim(), "md5");

    user.Password = hash;
    repository.Save();

    return true;
}

Notice that I didn't use the CreateUser method that comes with the MembershipProvider, I overloaded it so it will accept just the fields our database is interested in and then I passed the workload off to the repository.

Tying our Custom Providers into MVC

We've created our custom providers so the next step is to configure our application to use these instead of the default ones that ship with ASP.NET. Open your Web.Config file which exists at your project root. Notice that the connectionStrings section should contain 2 entries: ApplicationServices (the default connection string) and LocalBankEntities (the one that was added when we created our model). We will no longer be using ApplicationServices so you can delete this entry. First let's modify our membership provider to use our custom provider. Replace the membership section with the following code.

<membership defaultProvider="LocalBankMembershipProvider">
  <providers>
    <clear />
    <add name="LocalBankMembershipProvider" type="LocalBank.Helpers.LocalBankMembershipProvider, LocalBank" connectionStringName="LocalBankEntities" />
  </providers>
</membership>

First you must specify a defaultProvider which matches the name of the class which extends the MembershipProvider, in our case this is the LocalBankMembershipProvider. When specifying the type you pass the fully qualified path to your membership class followed by a comma and the name of the assembly it is contained in (aka the project name in most cases). And last but not least we need to specify where the data exists for authenticating membership. We'll be using the LocalBankEntities connection string which was added when we created our model. Next let's modify our roleManager to use our custom provider. Replace the roleManager section with the following code.

<roleManager defaultProvider="LocalBankRoleProvider" enabled="true" cacheRolesInCookie="true">
  <providers>
    <clear />
    <add name="LocalBankRoleProvider" type="LocalBank.Helpers.LocalBankRoleProvider, LocalBank" connectionStringName="LocalBankEntities" />
  </providers>
</roleManager>

This is nearly identical to our membership provider except that we need to specify that the role manager is enabled and that we would like to cache the users role in a cookie. This allows role management to check the cookie for role verification before calling the role provider. By default the roles are encrypted inside the cookie and verification is made to make sure they have not been altered. To modify this behavior you can use the CookieProtectionValue property, but this won't be nessesary for this project.

Modifying the AccountController to use our new Providers

By default MVC 3 uses the System.Web.Security.Membership class to authenticate users which we'll need to change in order to use our new membership provider. Let's start by declaring an instance of both providers in our AccountController.

public LocalBankMembershipProvider MembershipService { get; set; }
public LocalBankRoleProvider AuthorizationService { get; set; }

protected override void Initialize(RequestContext requestContext)
{
    if (MembershipService == null)
        MembershipService = new LocalBankMembershipProvider();
    if (AuthorizationService == null)
        AuthorizationService = new LocalBankRoleProvider();

    base.Initialize(requestContext);
}

The first change we'll need to make is to the LogOn HttpPost action. Replace the usage of the Membership class to use our MembershipService we just created. The code fragment will look something like this:

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {

Next we'll want to change our Register HttpPost action. We don't need to use the MembershipCreateStatus or the Membership classes, instead we will use our own. Here is my implementation.

[HttpPost]
public ActionResult Register(RegisterModel model)
{
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        try
        {
            MembershipService.CreateUser(model.UserName, model.Name, model.Password, model.Email, "Member");

            FormsAuthentication.SetAuthCookie(model.UserName, false);
            return RedirectToAction("Index", "Home");
        }
        catch (ArgumentException ae)
        {
            ModelState.AddModelError("", ae.Message);
        }
    }

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

Notice that I am passing the role name to associate with this user to CreateUser. Because this register method is openly exposed to anyone who wants to sign up on our bank site I hardcoded a default value of "Member". You may have other views which allow other types of roles or even an admin controll which allows the user to select the role for a user. I may post about this if there is any demand.

Finally, we'll want to change the ChangePassword HttpPost action to use our membership provider. This is pretty straight forward since we overrid this method.

[Authorize]
[HttpPost]
public ActionResult ChangePassword(ChangePasswordModel model)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ChangePassword(User.Identity.Name, model.OldPassword, model.NewPassword))
            return RedirectToAction("ChangePasswordSuccess");
        else
            ModelState.AddModelError("", "The current password is incorrect or the new password is invalid.");
    }

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

You can also remove the entire "Status Codes" region from your controller as it's no longer being used. You're almost there, but you'll notice that the project doesn't build currently because we are trying to pass model.Name to CreateUser when a user registers and we haven't defined this field yet. This is needed because our original database model requires a name but the built in RegisterModel does not. We'll correct this by making 2 small changes. First, add the following code to our RegisterModel class.

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

We'll also make a small change to our Register view so that a field appears for users to enter thier name. Add the following code to the Register view's form fieldset.

<div class="editor-label">
    @Html.LabelFor(m => m.Name)
</div>
<div class="editor-field">
    @Html.TextBoxFor(m => m.Name)
    @Html.ValidationMessageFor(m => m.Name)
</div>

Run the application and register yourself with the website. Did you get an error message?

Missing roles error

We haven't created any roles! This is not really part of this tutorial but just a reminder that you should create your roles either outside of your project using a script or by creating an admin section for your site which will allow you to manage users and roles. Again, I'd be willing to post more about this if there is interest.

Using our Role and Membership Providers

Now that we have created our providers and tied them into our application how do we use them? Well, there are a number of ways to do it depending on where you are and what preferences you have. Here are some short examples of using both in different situations.

Making sure a user is logged in to gain access to a view

The easiest way to accomplish this is to use the Authorize attribute above a controller's action method. For instance, we only want to allow users to change thier password if they are already logged into the site. In order to prevent non-authorized users from reaching the change password view we can restrict access like this:

[Authorize]
public ActionResult ChangePassword()
{
    ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
    return View();
}

You can also accomplish this manually by checking against the User object like this:

public ActionResult ChangePassword()
{
    if (!User.Identity.IsAuthenticated)
        return RedirectToAction("LogOn", "Account");

    ViewData["PasswordLength"] = MembershipService.MinPasswordLength;
    return View();
}

This checks to see if the user is authenticated and if not redirects them to the LogOn view. I would not recommend using this method however; the built in Authorize attribute will automatically redirect non-authorized users to the LogOn view specified in your Web.Config file. Once the user logs in it will automatically redirect them back to the previous page.

Making sure a user is in a particular role to gain access to a view

You may have some views which should only be accessable to users of a particular role. This can also be accomplished using the Authorize attribute like this:

[Authorize(Roles = "Administrator")]
public ActionResult Index()
{
    return View();
}

You can accomplish this in code as well using the following method:

public ActionResult Index()
{
    if (!User.IsInRole("Administrator"))
        return RedirectToAction("LogOn", "Account");

    return View();
}

Again, this is not recommended for the same reasons as before.

Checking authentication and roles in a view

I am using the Razor view engine so this may be a bit different than your code but the principles are the same. You may not want to stop a user from accessing a view but you may want to hide/show content based on a users authentication or role. This can be accomplished using code such as this:

<li>@Html.ActionLink("Home", "Index", "Home")</li>
@if (Request.IsAuthenticated)
{
    <li>@Html.ActionLink("View Account", "Index", "Account")</li>
    if (User.IsInRole("Administrator"))
    {
        <li>@Html.ActionLink("Admin Console", "Index", "Admin")</li>
    }
}

As you can see the Request object will allow us to verify if a user is authenticated and we also have a User object which allows us to check for roles. Notice in this code snippit that anyone visiting the site will get a Home link, authenticated users will also get a View Account link and authenticated administrator users will also see an Admin Console link.

Please leave feedback if this helped. This is my first blog entry so I'm not sure if I was too detailed or not enough. Also, if you find any errors or issues please let me know. I plan to write more entries similar to this one so let me know if there is anything you'd like to see. Thanks for reading!

Full Source: LocalBank.zip (2.87 mb)

Tags: , , ,

Comments (64) -

1.
Hang Lothrop Hang Lothrop United States says:

guy do more vlogs it looks so professional ROCK Upon!

2.
Keith S. Safford Keith S. Safford United States says:

Just what I was looking for.  My only modification will be to allow some folks to have more than one roles.  There will be my Users table, Roles table and UsersToRole intersect table.  But this gets me started to use something other than the dreaded aspnetdb.mdf.  Great job.

3.
Ursula Laur Ursula Laur United States says:

I need to say your site is interesting I really like your theme! Today I don’t have the free of charge time at the current moment to fully look via your sitebut I’ve bookmarked it. I will be back in a day or 2 . Many thanks for a good website.

4.
amir hosein jeodari amir hosein jeodari Iran says:

Hi ...  thanx for awesome post! ...
would you mind give me download link foe this example?!

5.
Brian Legg Brian Legg United States says:

Amir - Good idea. I updated the post to contain the full source. Check near the bottom of the post.

6.
Lev Lev Israel says:

Thank you very much!!! Perfect work.

Small remarks to attached code:
1. Change namespace  Schoolyard.Providers to namespace LocalBank.Helpers
2.  public override int MinRequiredPasswordLength
        {

            //  get {  }
            get { return MinPasswordLength; }

        }

7.
Brian Legg Brian Legg United States says:

Nice catch Lev. I fixed the error in the source download.

8.
sam sam United Kingdom says:

im getting when i register....
Server Error in '/' Application.

The method or operation is not implemented.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.NotImplementedException: The method or operation is not implemented.

Source Error:


Line 165:        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
Line 166:        {
Line 167:            throw new NotImplementedException();
Line 168:        }
Line 169:

9.
Brian Legg Brian Legg United States says:

Sam - Make sure you are calling the right CreateUser method in the membership provider. It looks as though you are calling the default CreateUser method. If you step into that method you should see that it throws a NotImplementedException right away. This method has been overloaded and only takes 5 parameters, not 8 like the one you're calling. Here is the method signature you should be calling:

MembershipService.CreateUser(model.UserName, model.Name, model.Password, model.Email, "Member");

Notice it does not take passwordQuestion, answer, isApproved, etc. If you would like to use the method you are calling now then you'll need to implement it in your MembershipProvider class.

Let me know if this helps.

EDIT: Take notice of the "Creating a Custom MembershipProvider" section. Specifically this text, "Notice that I didn't use the CreateUser method that comes with the MembershipProvider, I overloaded it so it will accept just the fields our database is interested in and then I passed the workload off to the repository." I think this is where you're stuck.

10.
sam sam United Kingdom says:

ive downloaded your source and just ran that, or do i need to apply the mods to it

11.
Brian Legg Brian Legg United States says:

Sam - Sorry for the late reply, I was out of town all weekend. I just looked a the code in the zip file and it looks like all of the membership provider/role provider code is there as well as the configuration and the data repository.... however, it seems I didn't include the updated Account controller. I think when I originally wrote this article I was thinking that people would write thier own controller implementation but I can see where that would be confusing if the code example isn't fully functioning. Give me a couple hours and I will get this fixed and uploaded.

12.
Brian Legg Brian Legg United States says:

Sam - I've made the necessary changes and uploaded the new zip file. The only thing to note is there are no roles created. If you look inside the AccountController Register HttpPost action you'll notice that any user who registers through this site is given a default role of "Member". This can be changed, but whatever role this is must first already be created in the Role table. You can do this by writing a script (most likely preferred method) or you can write some hacky code and call it real quick since it will only need to be ran 1 time when first creating roles. Here's some example code you can use to create the "Member" role, modify it to suit your needs.

LocalBankRepository repo = new LocalBankRepository();
repo.AddRole("Member");
repo.Save();

Call those 3 lines from somewhere in your code (or a separate "initializer" project) and from then on the code should work as expected. Sorry for the confusion. Let me know if you run into any problems.

13.
sam sam United Kingdom says:

Cheers Brian!

14.
shaikh naim shaikh naim India says:

Brian great post. Help me a lot in developing my custom membership and role provider.

15.
Paul Paul United States says:

Brian: Great article. Well written. Very complete and thorough. This helped me a TON ! ! !

16.
Bot Bot Philippines says:

nice post.. so far your blog is the nicest and easy to follow custom membership provider that I've read..
cheers!! keep up the good work dude.. can't wait for your next post.. Smile This is exactly what I really need..

17.
Bot Bot Philippines says:

Dude I tested your code and found some bug.. First I created an
account in Account/Register, the account works fine in Account/Logon..
Then I tried to create another account inside the User/Create, the
account that I've created can not enter in Account/LogOn.. I tried to
fix this myself but failed to do so.. hope to help me and update your
codes soon.. Your site is a guiding light for us growing plants who are
just starting to bloom.. Smile

18.
Vedran Vedran Croatia says:

Hey Brian, firstly BIG THANKS FOR THIS, Smile

It was a great tutor to read and code, and to understand how forms auth. works from the code behind.

Well, as Bot said, I have the same bug, or maybe it is not a bug, but the user is not inserted in to my User table in my database... In runtime of my app, when I create a new user and logout, then try to login the data is not persisted within the application nor the database table User.

I tried checking the entity model code but it all seems ok over there, I've got no ideas left, can you please help... Smile

10Q,
V.

19.
Vedran Vedran Croatia says:

Hey again... heh... I think I was a bit lazy to debug "deeper", then I found out that in the Repository class I added an extra check for Roles, which is unnessesary.

Everything works fine now, the users are in DB.

Thank you one more time! Smile

20.
Bot Bot Philippines says:

Hey dude.. Sorry for my previous comment..

I understand now why is that happening..

there's nothing wrong with your codes.. Smile

Your codes works perfectly...!!

great job dude.. ",) I'll be waiting for

your next topic.. You're the best men..

Cheers!!!

21.
Brian Legg Brian Legg United States says:

I'm glad everyone is getting something out of my post. This is by far the most popular post I've written. For those who have found "bugs" but later figured out what was going on... please post your solutions or what you were doing wrong. I think it could help others who end up having the same issue. I haven't been able to post any new articles for a while because I recently moved across country to FL and I started a new job. But very soon here I plan to devote some time to entity framework and covering some common issues related to using it. Thanks again for reading, and I'm always up for suggestions. I'm no expert by far and most of these articles are written as I learn and understand a topic, so suggestions to write others will help me to learn too.

22.
Bot Bot Philippines says:

Dude can I ask for a favor??

I've followed your custom role provider and glad to make it work..

But I want to add something like UserRole between the User and Role relationship..

I'm removing the link of User and Role and use UserRole instead to link both of them..

It has a properties of Id, RoleId and UserId.. The assigning of role will take place

in the UserRole.. I'm a little confuse what to change in your example and how to make

the authentication of role to take place on the UserRole.. Please help me dude..

Thanks in advance.. Smile

23.
Mic Mic United States says:

I have noticed in the code that you used forms authentication.  Our MVC web site uses a different form of authentication other than forms or windows it uses CAC.  Can the user models be ommitted and just the role models used?  Also can MVC 3 run on an IIS 6.0 server?

24.
Mike Clarke Mike Clarke Australia says:

I love your work, I have been looking for days for something like this. I’ve been building “Membership” whatevers for 15 years in Classic ASP, Webforms, Java, C++ and now just wanted something that works in MVC3. I tried everything I could think of to work with the given aspnet.mdf to integrate it into my projects and to no avail, I think it is there to confuse more than anything. I was at the end of my rope and just going to do what I’ve always done and build my own from scratch!

I have recently asked the question regarding this tutorial, I will now go back and answer my own question and point a link to your site, if that’s ok?
Like I said, I love your work and thanks.

Mike.

25.
Gaurav Gaurav United States says:

Hi Brian,

Is the re-use of the code allowed? I am developing an application for a client and need a quick implementation.

Thanks & Regards,
Gaurav

26.
Brian Legg Brian Legg United States says:

@Bot - I'm glad you liked the tutorial and I'd love to implement something like you're talking about. I just moved across the country and I've started a new job so my time is very limited right now. Hopefully I'll get to writing that as a followup at some point soon.

@Mic - I'm sorry Mic, but I have no idea. I'm pretty new to web development in general and I like to post articles as I figure out different things which were difficult for me. I'm not too sure about the IIS limitations or CAC, sorry.

@Mike - I'm glad you enjoyed my post, feel free to post a link to it, I don't mind. Smile

@Gaurav - Use the code however you'd like. I'm glad you liked the post.

27.
Bot Bot Philippines says:

Thanks dude.. I'll always be checking your site Smile.. Godspeed to your new job..

28.
johnl johnl United Kingdom says:

really great post!! it was very helpful to me - i've just got to convert it to vb!!
more on multiple roles and membership would be great - keep up the good work

29.
Steven Steven New Zealand says:

Just found this blog, its like a gem. thank you so much for sharing this.

30.
Gonzalo Gonzalo Chile says:

I was following your project and want to know.
How to create another cookie with the UserID. Thank you.

31.
Kani Kani Canada says:

Hi Brian,

Your code is working very well. I would like to know if this code will work for the MVC 4 version or you have to make change.

Thanks for your help.

32.
Brian Legg Brian Legg United States says:

I haven't had a chance to play around with MVC4 just yet. I don't think it would be an issue though. Most of the code here is really MVC independent and is using the role providers and such within .NET framework. The views would change and maybe some validation code would be different but the core role provider and membership provider should remain the same.

Let me know if you try it out, I'm interested to know how it goes.

33.
Mike Clarke Mike Clarke Australia says:

I’ve only used it in MVC4 and it works perfectly!

Cheers,

Mike.

34.
Kani Kani Canada says:

I started to use MVC 4. I have only problem: [Autorize(Roles = "Member")] or [Autorize(Roles = "Administror")] are not working well. [Autorize(Users = "myuser")] is working.

35.
Vladimir Vladimir Russia says:

Hi all,
for MVC3 exellent example
thanks
Brian, could you help me add FullUserName on LogOnUserControl
in database it is "Name" field of "User" table

36.
Has Has Australia says:

Thanks heaps, this tute was very useful for me. Really appreciate it, and please do keep up the good work Smile

37.
Sean Dooley Sean Dooley United Kingdom says:

This article is exactly what I was looking for! Clear, concise and easy to follow! Great work!

38.
angela flowers angela flowers United States says:

I am getting method not implement in asp.net admin. tools, and it is not saving to the database I get the same thing when I run your code.

39.
Bhanu Prakash Inturi Bhanu Prakash Inturi India says:

Article is very useful and its very good to follow

Can we have the same above example in Code First Approach

Thanks
Bhanu Prakash Inturi

40.
colau colau Spain says:

Hi, will you post how create roles? It will be so perfect post Brian!

Thanks a lot.

41.
Abbas Abbas United Kingdom says:

This is a great tutorial, I was looking for it, thanks mate

42.
AndrX AndrX United States says:

super awesome tutorial!

43.
George Tatar George Tatar Romania says:

Thanks a lot !!! This tutorial helps me to resolve my authentications  problems!!!

44.
Tochisan Tochisan United States says:

Thanks so much for this tutorial!  This helped answer a lot of questions I had about custom Membership and Role providers... now to tackle custom profile providers...  Any suggestions?

45.
sunil shrestha sunil shrestha Nepal says:

Awesome tutorials! you are god

46.
bond bond India says:

thanks Brian! for such wonderful guide but i get the issue as
The underlying provider failed on Open. Entity Exception was unhanded by user code when i try to register .i simply download the source code and create the two tables in sql server 2005 db  how to solve this and what is reason behind this thanks

47.
Tharan Ratnam Tharan Ratnam Canada says:

Thank you very much Brian for such a wonderful post. I am developing asp.net mvc 3 project with EF AND RAZOR.This post helped a lot for me.

48.
Jeff Tallent Jeff Tallent United States says:

Brian, this is an excellent tutorial. It gives the relevant details that often are glossed over by other posts--always the parts we need most, right? You do this succinctly and effectively allowing developers to reach that critical decision point: should I or shouldn't I? Careful explanations with samples and commentary on alternate ways to accomplish similar goals make this a wonderful guide. Thanks for taking the time to share!

49.
Michael Michael United Kingdom says:

Hi Brian. Thanks for this tutorial. I wonder if any have faced the situation I have whereby you want to implement not one but two (or more perhaps) custom role providers. The scenario is that the general users will use the one custom role provider but the administrators will use another role provider due to various reasons. I have been trying to override the AuthorizeAttribute with success and then apply it as the attribute to the controller action I want to protect - for example:
        [AdministrationAuthorise(Roles = "Administrator")]
        public ActionResult Index()
        {
            return View();
        }


But what I am finding is that despite adding this custom AuthorizeAttribute when the Administrator logs in and the site wants to now send them to the "Index" action (as in example above) the general user role provider is being called. I realise that this is probably due to the fact that it is specified in the web.config as the default role provider. But I just can't seem to work out how to specify a different role provider for this custom AuthorizeAttribute. Any help would be greatly appreciated.
Thanks
Michael

50.
Eduard Eduard Philippines says:

Hi Sir, very helpful article, i applied this with my project and it worked, thanks you for posting this. By the way, my custom roles are not working, i added roles direct to SQL.. but in your Project file it works if you call the "User.IsInRoll" code.. If I can request for an article including the view for custom role provider where users can add custom roles, also with the membership form. thank you very much.. God bless..

51.
Bala Bala India says:

Hi,

I am implementing Repository Pattern in one of my project based on ASP.NET MVC4 and N-Tier Architecture. I am little confused as how to implement Custom Membership with Repository Pattern. Here are how I have implemented my classes so far.

Generic Repository

public interface IRepository<T> where T : class
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IQueryable<T> GetAll();
T FindBy(Expression<Func<T, bool>> expression);
IQueryable<T> FilterBy(Expression<Func<T, bool>> expression);
}

Repository Base Class

public abstract class Repository<T> : IRepository<T> where T : class
{
private STNDataContext _stnDataContext;
private readonly IDbSet<T> _dbSet;

protected Repository(IDatabaseFactory databaseFactory)
{
DatabaseFactory = databaseFactory;
_dbSet = StnDataContext.Set<T>();
}

protected IDatabaseFactory DatabaseFactory { get; private set; }
public STNDataContext StnDataContext
{
get { return _stnDataContext ?? (_stnDataContext = new STNDataContext()); }
}
public void Add(T entity)
{
_dbSet.Add(entity);
_stnDataContext.Commit();
}

public void Delete(T entity)
{
_dbSet.Remove(entity);
}

public void Update(T entity)
{
_dbSet.Attach(entity);
_stnDataContext.Entry(entity).State = EntityState.Modified;
_stnDataContext.Commit();
}

public IQueryable<T> GetAll()
{
return _dbSet.ToList().AsQueryable();
}

public T FindBy(Expression<Func<T, bool>> expression)
{
return FilterBy(expression).SingleOrDefault();
}

public IQueryable<T> FilterBy(Expression<Func<T, bool>> expression)
{
return GetAll().Where(expression).AsQueryable();
}

User Repository Interface

public interface IUserRepository : IRepository<User>
{
}

User Repository

public class UserRepository : Repository<User>, IUserRepository
{
public UserRepository(IDatabaseFactory databaseFactory)
: base(databaseFactory)
{
}
}

Heres my Business Logic Layer

Generic Service Interface

public interface IService<T>
{
void Add(T entity);
void Delete(T entity);
void Update(T entity);
IEnumerable<T> GetAll();
T FindBy(Expression<Func<T, bool>> expression);
IEnumerable<T> FilterBy(Expression<Func<T, bool>> expression);
}



Service Base

public class Service<T> : IService<T> where T : class
{
public void Add(T entity)
{
throw new NotImplementedException();
}

public void Delete(T entity)
{
throw new NotImplementedException();
}

public void Update(T entity)
{
throw new NotImplementedException();
}

public IEnumerable<T> GetAll()
{
throw new NotImplementedException();
}

public T FindBy(Expression<Func<T, bool>> expression)
{
throw new NotImplementedException();
}

public IEnumerable<T> FilterBy(Expression<Func<T, bool>> expression)
{
throw new NotImplementedException();
}
}



User Service Interface

public interface IUserService : IService<User>
{
}

User Service Implementation

public class UserService : Service<User>, IUserService
{
private readonly IUserRepository _userRepository;
private readonly IRoleRepository _roleRepository;

public UserService(IUserRepository userRepository, IRoleRepository roleRepository)
{
_userRepository = userRepository;
_roleRepository = roleRepository;
}

public IList<User> GetAllUsers()
{
return _userRepository.GetAll().ToList();
}

public User GetUser(int id)
{
return _userRepository.FindBy(x => x.UserId == id);
}

public User GetUser(string userName)
{
return _userRepository.FindBy(x => x.Username.Equals(userName));
}

public void CreatUser(User user)
{
_userRepository.Add(user);
}

public IList<User> GetUsersForRole(string roleName)
{
return _userRepository.FilterBy(x => x.Role.RoleName.Equals(roleName)).ToList<User>();
}

public IList<User> GetUsersForRole(int roleId)
{
return _userRepository.FilterBy(x => x.Role.RoleId == roleId).ToList<User>();
}

public IList<User> GetUsersForRole(Role role)
{
return GetUsersForRole(role.RoleId);
}

}
Hi Bob,
I am developing one application using ASP.NET MVC4, N-Tier Architecture and Repository Pattern. I want to implement Custom Membership functionality and refering your gpsnerd application as reference. I am little confused implementing the same. Can you help?

I am not very sure on Am I going the right way? If yes how do i implement my UserService class. 

If no, what changes do i need to implement.

Any help on this is highly appreciable. Thanks in advance

52.
Sreeram Ganganath Sreeram Ganganath India says:

Hai Can i use the same to do in MVC2 while i am trying this in mvc2 i cannot get  System.Web.Security.RoleProvider can ou please help me. Thanks in advance.

53.
Romias Romias Uruguay says:

Brian, any way to pass more parameters to a RoleProvider?
The RoleProvider get the user roles just using the username, and I need to use the username and TenantId to verify roles.

Any idea on how to accomplish this?

54.
Revo Revo United States says:

Well done! Your article is excellently written and I have benefited from it immensely. Thanks and keep it up!

55.
Kanta Kanta Croatia says:

you sir, deserve a cookie, or a whole box of 'em.. any brand you like, ofc..

56.
Yobiwoo Yobiwoo Malaysia says:

Hi Brian, this is a great sample & tutorial. I would like ask how can i have multiple roles for a User?Hope anybody can guide me.Thanks

57.
CyclingFoodmanPA CyclingFoodmanPA United States says:

Yobiwoo,

I have a sample using the Northwind DB that implements multiple roles for users.  Email me at ks.safford@verizon.net or ksafford@guttmanoil.com and I'll send you a zipped up MVC 3 example utilizing multiple roles and a number of other cool things I did for a prototype.

58.
Yobiwoo Yobiwoo Malaysia says:

Hi CyclingFooodmanPa, thank so much for your reply & i have email you as per your request.Really cool to here that u have many prototype.sure it will be very useful for me.Thank u so much.May God Bless u...

59.
hamd hamd Afghanistan says:

Hey man I am really new to .Net, but this tutorial was a piece of cake for me I leared advance stuff in very
easy and beggining way!in

60.
Randy Randy United States says:

This is great!  However, I can't figure out how to update a user. We have a screen allowing them to edit stuff they added before.  There's no ModifyObject like the add one: entities.Users.AddObject(user);

Any idea how to do this?

61.
Brian Legg Brian Legg United States says:

Have you tried simply modifying the properties and calling Save? It's been a long time since I wrote this so I'm not sure if the entities are bound to the data context or not. If that doesn't work you may need to create a new method off the repository to modify the properties directly off the context instance then save. Anything added/removed/modified off the "entities" context will be persisted once you save. Does this make sense or help at all?

62.
rashid rashid Azerbaijan says:

hi brian thanks its excellent

63.
Damian Damian United Kingdom says:

Thank you so much for this... a real life saver I've implemented it using code first and I thought it was going to be a nightmare, but this was really simple to follow and has got the job done... In terms of what Randy was asking about updating a user I have a repository method called CreateUpdate that accepts a person object... it seems to do the job:

[Code Sample]

public void CreateUpdate(Person person)
        {

            if (person.PersonID == default(int))
                context.Persons.Add(person);
            else
                context.Entry(person).State = EntityState.Modified;
        }

[End Code Sample]

hope this is helpful

65.
yashaswini yashaswini India says:

<roleManager defaultProvider="LocalBankRoleProvider" enabled="true" cacheRolesInCookie="true">
        <providers>
            <clear />
            <add name="LocalBankRoleProvider" type="LocalBankRepository.Helpers.LocalBankRoleProvider, LocalBank" connectionStringName="LocalBankEntities" />          
        </providers>
</roleManager>
    <membership defaultProvider="LocalBankMembershipProvider">
        <providers>
            <clear />
            <add name="LocalBankMembershipProvider" type="LocalBankRepository.Helpers.LocalBankMembershipProvider, LocalBank" connectionStringName="LocalBankEntities" />          
       </providers>
</membership>


I added these in web.config and i'm not able to understand why i'm getting this error.. Frown

Configuration Error
Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Parser Error Message: The configuration section cannot contain a CDATA or text element.

Source Error:


Line 39:       </providers>
Line 40:     </roleManager>
Line 41:     <membership defaultProvider="LocalBankMembershipProvider">
Line 42:         <providers>
Line 43:             <clear />


I am new to MVC and trying to learn if you could help out in this I will be glad..

Pingbacks and trackbacks (1)+

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading