Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom Role Providers

0.00/5 (No votes)
19 Jun 2013 1  
An introduction to custom role providers in an ASP.NET MVC 3 application using the Entity Framework for ORM.

Default home page

Introduction

Just like authentication being critical for web applications, roles are also essential for a number of reasons. For instance, roles could be used to limit the availability of certain features of an application to a certain group of users. This entire article rests on the RoleProvider abstract class (MSDN), which is provided by ASP.NET MVC. By the end of this article, you will be able to take the sample application provided in part 1 of the article (download) and segregate the pages based on three roles - "Super Admin", "Admin", and "Author".

Default username/password for logging in (the super admin user): administrator / administrator

Background

I earlier wrote an article about using custom membership providers and I received a lot of comments. One of the comments was to follow up with an article about custom role providers. This article could be considered a "sequel" to my previous article about custom membership providers! I have used ASP.NET MVC 3 to explain about custom role providers and have used the Entity Framework for the data layer.

A Few Words About Users and Roles

In order to control access to a certain action method, you would use the Authorize attribute as shown below:

[Authorize]
public ActionResult Index()
{
    return View();
}

The Authorize attribute just controls access to a certain action method, Index in this case. If a user is logged in, they get to see the page. If not the user is redirected to the login page specified in the web.config file or by code. You can limit access to a page for only a certain user by passing in a User parameter as shown below. In this case, access to this page is limited to a user with the username admin.

[Authorize(Users = "admin")]
public ActionResult Index()
{
    return View();
}

If another user (say, karthik) has to access this page, the Users parameter has to be changed to include the second user too as shown below:

[Authorize(Users = "admin,karthik")]
public ActionResult Index()
{
    return View();
}

From the modified version, I guess it is evident that this method does not scale well. What if there are 100 users requiring access to this page? What if new users signing up will be required to access this page? It is crazy to even think of manually changing the attribute to include the users we need. Note that whenever this is changed a new build is required! So, what do we do? "Roles" is the answer! Let's get to the most interesting part of this article now :)

The Prerequisites - Database and the Data Access Layer

This section is just a precursor to the main stuff! To prepare your database, create a new database in your MS SQL Server and run the Setup.sql within the downloaded source. This will create three tables:

  • Users - to hold the users
    CREATE TABLE [dbo].[Users](
        [UserId] [int] IDENTITY(1,1) NOT NULL,
        [UserName] [varchar](50) NOT NULL,
        [Password] [varchar](50) NOT NULL,
        [UserEmailAddress] [varchar](50) NOT NULL,
     CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED 
    (
        [UserId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
        IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
  • Roles - to hold the roles valid for this application
    CREATE TABLE [dbo].[Roles](
        [RoleId] [smallint] NOT NULL,
        [RoleName] [varchar](50) NOT NULL,
        [RoleDescription] [varchar](255) NOT NULL,
     CONSTRAINT [PK_Roles] PRIMARY KEY CLUSTERED 
    (
        [RoleId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    /* Roles table entries */
    INSERT INTO dbo.Roles VALUES(0,'SuperAdmin','Super Admin');
    INSERT INTO dbo.Roles VALUES(1, 'Admin', 'Administrator');
    INSERT INTO dbo.Roles VALUES(2, 'Author', 'Blog Author');
  • UserRoles - to hold the roles associated to a user
    CREATE TABLE [dbo].[UserRoles](
        [UserRoleId] [int] IDENTITY(1,1) NOT NULL,
        [UserId] [int] NOT NULL,
        [RoleId] [smallint] NOT NULL,
     CONSTRAINT [PK_UserRoles] PRIMARY KEY CLUSTERED 
    (
        [UserRoleId] ASC
    )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
      IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
    ) ON [PRIMARY]
    
    GO
    
    ALTER TABLE [dbo].[UserRoles]  
       WITH CHECK ADD  CONSTRAINT [FK_UserRoles_Roles] FOREIGN KEY([RoleId])
    REFERENCES [dbo].[Roles] ([RoleId])
    GO
    
    ALTER TABLE [dbo].[UserRoles]  
         WITH CHECK ADD  CONSTRAINT [FK_UserRoles_Users] FOREIGN KEY([UserId])
    REFERENCES [dbo].[Users] ([UserId])
    GO

And, a default user is also added with the username administrator and password administrator. Correspondingly an entry is added to the UserRoles table for the default user: this user is added as the super admin so that other users can be managed!

The zip file provided as a starting point in the introduction already has a Setup.sql file that contains the SQL command required to create the Users table. If you plan to use the same database, please don't forget to comment out the part that creates the Users table in this script. It also contains a User class representing a user in the Users table. Now I am creating two more classes, one to represent a Role and one to represent a UserRole and also modifying the User class to have a link to the roles assigned to this user (remember, this data access layer is using Entity Framework). They are given below. Notice the UserRoles property in User which can be used to find the roles associated to the user. Also notice the Role property in the UserRole class that can be used to identify more information about a role. Finally [Key] is an attribute provided by Entity Framework to indicate that this property is the primary key for this table. Since it's out of the scope for this article to discuss about Entity Framework I am skipping that.

public class User
{
    [Key]
    public int UserId { get; set; }
    public string UserName { get; set; }
    public string Password { get; set; }
    public string UserEmailAddress { get; set; }

    public virtual ICollection<UserRole> UserRoles { get; set; }
}

public class Role
{
    [Key]
    public short RoleId { get; set; }
    public string RoleName { get; set; }
    public string RoleDescription { get; set; }
}

public class UserRole
{
    [Key]
    public int UserRoleId { get; set; }
    public int UserId { get; set; }
    public short RoleId { get; set; }

    public virtual Role Role { get; set; }
}

The next file that changes is the UsersContext which acts as our repository class. This class has various properties that help us access the three tables and also provides helper methods like AddUser. This class is given below, discussion follows after the listing!

public class UsersContext : DbContext
{
    public DbSet<User> Users { get; set; }
    public DbSet<Role> Roles { get; set; }
    public DbSet<UserRole> UserRoles { get; set; }

    public void AddUser(User user)
    {
        Users.Add(user);
        SaveChanges();
    }

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

    public User GetUser(string userName, string password)
    {
        var user = Users.SingleOrDefault(u => u.UserName == 
                       userName && u.Password == password);
        return user;
    }

    public void AddUserRole(UserRole userRole)
    {
        var roleEntry = UserRoles.SingleOrDefault(r => r.UserId == userRole.UserId);
        if (roleEntry != null)
        {
            UserRoles.Remove(roleEntry);
            SaveChanges();
        }
        UserRoles.Add(userRole);
        SaveChanges();
    }
}

To this class I have added two new properties: Roles and UserRoles which are DbSet - which are the properties used to access the two new tables. Then I have added one more method AddUserRole that can be used to add a role entry for a user. The next class is quite important. This next class I have added holds a lot of useful information about the user currently logged in - UserIdentity.

public class UserIdentity : IIdentity, IPrincipal
{
    private readonly FormsAuthenticationTicket _ticket;

    public UserIdentity(FormsAuthenticationTicket ticket)
    {
        _ticket = ticket;
    }

    public string AuthenticationType
    {
        get { return "User"; }
    }

    public bool IsAuthenticated
    {
        get { return true; }
    }

    public string Name
    {
        get { return _ticket.Name; }
    }

    public string UserId
    {
        get { return _ticket.UserData; }
    }

    public bool IsInRole(string role)
    {
        return Roles.IsUserInRole(role);
    }

    public IIdentity Identity
    {
        get { return this; }
    }
}

The above class contains various properties like UserId, Name, IsAuthenticated etc. Apart from these another interesting method is IsInRole which I will get back to later, but in simple words as the method content implies, it calls the IsUserInRole method in the Roles class to see whether the user belongs to a certain role and returns it. If you notice, the constructor of this class gets a FormsAuthenticationTicket instance which is the decrypted version of the cookie that you will be seeing in the next section. More on this later!

The Prerequisites - Wiring Up the Logon Action

In this section I am just continuing the nitty gritty details of the pre-requisites :) The download discussed in the Introduction does not do anything after a user is successfully authenticated. But I cannot afford to do that anymore! So I hook in to the PostAuthenticateRequest event to do some custom processing. The following section shows you the stuff carried out in this event!

void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
    var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            var ticket = FormsAuthentication.Decrypt(encTicket);
            var id = new UserIdentity(ticket);
            var prin = new GenericPrincipal(id, null);
            HttpContext.Current.User = prin;
        }
    }
}

In the listing above, I get the authentication cookie, decrypt the ticket, and create an instance of the UserIdentity class described earlier. Then an instance of GenericPrincipal is created, which is then stored in the HttpContext.Current.User property. This is now accessible throughout the application! This event is fired when a user is successfully authenticated during login as shown below. Before the event is fired, an authenticated ticket is created (line 36), encrypted (line 37), and stored in the cookie collection (line 38). Check out the following listing from AccountController:

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (MembershipService.ValidateUser(model.UserName, model.Password))
        {
            SetupFormsAuthTicket(model.UserName, model.RememberMe);
            // -- Snip --
            return RedirectToAction("Index", "Home");
        }
        ModelState.AddModelError("", 
          "The user name or password provided is incorrect.");
    }
    return View(model);
}

// -- Snip --

private User SetupFormsAuthTicket(string userName, bool persistanceFlag)
{
    User user;
    using (var usersContext = new UsersContext())
    {
        user = usersContext.GetUser(userName);
    }
    var userId = user.UserId;
    var userData = userId.ToString(CultureInfo.InvariantCulture);
    var authTicket = new FormsAuthenticationTicket(1, //version
                        userName, // user name
                        DateTime.Now,             //creation
                        DateTime.Now.AddMinutes(30), //Expiration
                        persistanceFlag, //Persistent
                        userData);

    var encTicket = FormsAuthentication.Encrypt(authTicket);
    Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
    return user;
}

Creating a Custom Role Provider

Now it's time for the actual fun part! In this section I am going to discuss about the actual custom role provider itself! My custom role provider will extend the RoleProvider class provided ASP.NET MVC. Even though this class has a number of methods, in this article I am just going to concentrate on a few methods. So the following listing only contains those methods.

public class CustomRoleProvider : RoleProvider
{
    public override bool IsUserInRole(string username, string roleName)
    {
        using (var usersContext = new UsersContext())
        {
            var user = usersContext.Users.SingleOrDefault(u => u.UserName == username);
            if (user == null)
                return false;
            return user.UserRoles != null && user.UserRoles.Select(
                 u => u.Role).Any(r => r.RoleName == roleName);
        }
    }

    public override string[] GetRolesForUser(string username)
    {
        using (var usersContext = new UsersContext())
        {
            var user = usersContext.Users.SingleOrDefault(u => u.UserName == username);
            if (user == null)
                return new string[]{};
            return user.UserRoles == null ? new string[] { } : 
              user.UserRoles.Select(u => u.Role).Select(u => u.RoleName).ToArray();
        }
    }

    // -- Snip --

    public override string[] GetAllRoles()
    {
        using (var usersContext = new UsersContext())
        {
            return usersContext.Roles.Select(r => r.RoleName).ToArray();
        }
    }

    // -- Snip --
}

Let's consider the IsUserInRole method first. In this method, I use the repository (UsersContext) that was described in the previous sections to get a reference to the user logged in, from the database. Note that this happens "after" a user is authenticated and hence there is no need for a password. If this step fails, a value of false is returned indicating that the user does not belong to this role. If I do find a user, I first see if the navigation property UserRoles has anything [MSDN]. If so, I use the navigation property available in UserRole to select the roles for this user and see if there is a role assigned to this user with the role name passed to the method (roleName).

The next method I am going to discuss is the GetRolesForUser method. Even in this method I first check if an entry with the username passed can be found. If so, I use the same navigation properties to find the roles assigned to the user and return them. The last method is GetAllRoles and I don't think you would even want me to explain this method!

Wiring up the Custom Role Provider

Okay, we now have written the custom role provider. But how does the framework know how to use this custom role provider? As you expected, the web.config takes care of this! Locate the roleManager section in the web.config file in the download provided. It looks like this:

<roleManager enabled="false">
  <providers>
    <clear/>
    <add name="AspNetSqlRoleProvider" 
       type="System.Web.Security.SqlRoleProvider" 
       connectionStringName="ApplicationServices" applicationName="/" />
    <add name="AspNetWindowsTokenRoleProvider" 
       type="System.Web.Security.WindowsTokenRoleProvider" 
       applicationName="/" />
  </providers>
</roleManager>

The above default has to be replaced with the following so that ASP.NET MVC knows the fully qualified name for our custom provider and also that the role manager has to be enabled with the providers in the list.

<roleManager enabled="true" defaultProvider="CustomRoleProvider">
  <providers>
    <clear/>
    <add name="CustomRoleProvider" 
       type="CustomMembershipEF.Infrastructure.CustomRoleProvider, 
             CustomMembershipEF, Version=1.0.0.0, Culture=neutral" 
       connectionStringName="UsersContext"
       enablePasswordRetrieval="false" enablePasswordReset="true" 
       requiresQuestionAndAnswer="false" writeExceptionsToEventLog="false" />
  </providers>
</roleManager>

The first thing to note is that I have set the enabled attribute to true so that the framework enables the role manager. Then you have to specify the defaultProvider attribute, which is used to identify the default provider if a number of providers are specified. But in this case, I am going to have only one provider CustomRoleProvider, still the default provider has to be specified. This is contained within the providers element. The clear element is used to clear all the providers stored for this application earlier, for example the default providers. Then I have defined the custom role provider by specifying the name "CustomRoleProvider", which was used in the defaultProvider attribute. This contains a number of attributes. The most important one is the type attribute where the fully-qualified name of the custom role provider is specified (CustomMembershipEF.Infrastructure.CustomRoleProvider), followed by the assembly containing this type (CustomMembershipEF) and the version. Note that only the type name is required and others are optional if the type is contained within the same assembly - the web application itself. Other attributes are self-explanatory and I am not going to bore you with all these details!

Supplying the Roles for a User Post Successful Authentication

Okay, we have a role provider and its tied to the framework. What's next? Before I start doing anything, I have to let the framework know the roles of the authenticated user! This is where the custom role provider is used. Let's get back to the MvcApplication_PostAuthenticateRequest method in Global.asax.cs. If you recall this is an event fired after a user is successfully authenticated. Earlier, when I created an instance of GenericPrincipal, I was passing the user's identity as the first parameter and null as the second parameter. The second parameter represents the roles the user has and it is a string array. In the case of the sample application, a user can have only one role. But this is easily changeable. Since we need the roles, I cannot afford to pass in a null anymore. So I use the custom role provider's GetRolesForUser method to get the roles for this user. The updated event handler is shown below:

void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
    var authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie != null)
    {
        string encTicket = authCookie.Value;
        if (!String.IsNullOrEmpty(encTicket))
        {
            var ticket = FormsAuthentication.Decrypt(encTicket);
            var id = new UserIdentity(ticket);
            var userRoles = Roles.GetRolesForUser(id.Name);
            var prin = new GenericPrincipal(id, userRoles);
            HttpContext.Current.User = prin;
        }
    }
}

Notice line 11 and 12. In line 11, I call the GetRolesForProvider in the Roles static class. Then the roles for the user are passed on to the GenericPrincipal constructor while creating an instance of it. But wait, I know you are wondering where does the Roles class comes from suddenly. This is nothing but a reference to an instance of our custom role provider, CustomMembershipEF.Infrastructure.CustomRoleProvider, that the framework has created for us, since we specified this to be our custom role provider in the web.config file! Note that you don't have to change a single line of code, if you wish to create a new role provider, except changing it in web.config!

Few Words about the Authorize Attribute

Before I get started with the usage of the role provider, I guess its necessary to discuss a bit about the Authorize attribute (Part 1 and Part 2 has a more extensive discussion about the same). When an action method is decorated with the Authorize attribute, only logged in users can access this action method. If an unauthenticated user tries to access this action method, he/she will be redirected to the logon page. In the method given below, Protected method is decorated with the Authorize attribute. Once the user is authenticated, if you recall the changes to the Global.asax.cs file's MvcApplication_PostAuthenticateRequest, I stored a UserIdentity object in HttpContext.Current.User. Since the user is authenticated, this property can be used to access user specific properties.

[Authorize]
public ActionResult Protected()
{
    var user = (UserIdentity) User.Identity;
    return View((object)user.UserId);
}

If you notice, in line 3, HttpContext.Current.User is accessed as User implicitly. The Identity property of User is stored in user. Now just to show you how I could use one of the properties, I pass the UserId of the logged in user to the view as the model.

Using the Custom Role Provider

I know, you are wondering what's going to be in this section, since I have already used the custom role provider. But that's just the beginning! The uses of this custom role provider is manifold and let's see a few of them! Here is the first sample usage of the role provider!

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

Authorize attribute accepts a parameter called Roles, using which I set the roles allowed to access this action method. In this case the "SuperAdmin" role. So, if a user who is not logged in or a user who is logged in but does not belong to this role tries to access this action method, they are redirected to the login page!

Earlier, if you recall I discussed about setting the GenericPrincipal class in the "Supplying the Roles for a User Post Successful Authentication" section. So when an action method is decorated with the roles required or when User.IsInRole method is called, the roles passed are used by the framework to decide if the user can access the corresponding action method.

You could also pass a comma (,) separated list of roles in order to provide access to multiple roles for this action method. There is also another way by which you could check if the user belongs to a certain role before the user can access this action method. Its given below:

[Authorize]
public ActionResult AdminOrSuperAdmin()
{
    if (!User.IsInRole("SuperAdmin") && !User.IsInRole("Admin"))
    {
        return RedirectToAction("Index", "Home");
    }
    return View();
}

In the above example, instead of using the Roles parameter, I use the IsInRole method provided by the User object. If you recall, in the UserIdentity class, I implement the IPrincipal interface. This interface enforces the class to implement the IsInRole method and the ApplicationName property. In the IsInRole method, I use the Roles class to get the roles for the user and verify if the user has this role (explained earlier). As evident from line 3, if the user does not belong to either of "SuperAdmin" or "Admin", I redirect the user to the home page (unlike the login page if I use the Roles parameter. The same effect can also be achieved by the following:

[Authorize(Roles = "Admin, Author")]
public ActionResult AdminOrAuthor()
{
    return View();
}

Selectively Show Links to the User Based on Thier Role

Various ways by which we could check if a user belongs to a certain role can also be used in the views! For example, in my layout, I selectively show links for a user based on their role. Here is a section of that:

<div id="menucontainer">
    <ul id="menu">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Protected", "Protected", "Home")</li>
        @if (User.IsInRole("SuperAdmin"))
        {
            <li>@Html.ActionLink("Super Admin", 
              "SuperAdmin", "Home")</li>
        }
        @if (User.IsInRole("Admin"))
        {
            <li>@Html.ActionLink("Admin", "Admin", "Home")</li>
        }
        @if (User.IsInRole("SuperAdmin") || User.IsInRole("Admin"))
        {
            <li>@Html.ActionLink("Admin Or Super Admin", 
               "AdminOrSuperAdmin", "Home")</li>
        }
        @if (User.IsInRole("Admin") || User.IsInRole("Author"))
        {
            <li>@Html.ActionLink("Admin Or Author", 
              "AdminOrAuthor", "Home")</li>
        }
        @if (User.IsInRole("SuperAdmin"))
        {
            <li>@Html.ActionLink("Manage Users", 
              "Index","Manage")</li>
        }
    </ul>
</div>

As evident from the listing above, just like the AdminOrSuperAdmin action method, I use the IsInRole method of the User object to find if the currently logged in user belongs to a certain role. So in the layout the links show up according to the role to correspond with the action taken in the controllers.

Updating the Role of a User

I also have added a page using which the role of a user could be updated. The working of this page is simple: every user except user with id 1 is listed in this page. Every user has a corresponding drop down using which the role for the author can be set. From the table structure, I guess you realize a user can have multiple roles. But for the sake of simplicity, I have assumed a user can have only one role. Given below is a screenshot of this page.

Sample Image - maximum width is 600 pixels

The way this page works is simple and straight forward. When the dropdown corresponding to a user is changed (or not), and the "set" button is clicked an AJAX request is issued to the server and the role for the user is updated. Since this is outside the scope of the article I am going to leave it at that!

Next Steps

A number of improvisations are possible with respect to roles. For instance, there could be a Rights table that maps to a role in the Roles table. Ability to manage user for example is a "right". So, there would be an entry in the Rights table. With this approach, the UserIdentity class can be extended to see if a user can perform a certain function Or the same could be achieved from within the action method. Thus, there are a lot of avenues by which this project could be improvised and may be at some point I will post an update to this article based on this idea.

History

  • Version 1 of the article released.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here