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
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);
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError("",
"The user name or password provided is incorrect.");
}
return View(model);
}
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, userName, DateTime.Now, DateTime.Now.AddMinutes(30), persistanceFlag, 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();
}
}
public override string[] GetAllRoles()
{
using (var usersContext = new UsersContext())
{
return usersContext.Roles.Select(r => r.RoleName).ToArray();
}
}
}
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.
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.