Problem Space
Sad, but true, “Forms authentication in ASP.NET does not directly support role based authorization”. If you have ended up implementing Forms authentication along with configuring authorization rules for “users” and “roles” in the web.config, you are going to see the access rules working fine for “users”, but, not working at all for “roles”. You might have thought, there must be some way to specify user roles in the famous FormsAuthentication.RedirectFromLoginPage()
, or, any other method. But, there isn't!
Background
This is really surprising because, in real life, most applications (if not all) actually require authorization of system resources based upon user roles, not user names. So, if you are going to use Forms authentication in your upcoming ASP.NET application, and you need to implement role based authorization in your system, you have a problem.
Wait, this is not entirely true, because of two reasons:
Reason 1: Since ASP.NET 2.0, we have Membership. It includes Membership (User) service, Role service, and Profile (User properties) service. And, using Membership, you can easily implement Role based authorization in your ASP.NET application.
Reason 2: Even if you don't use Membership, you can write some code to implement Role based authorization in Forms authentication. Basically, you need to create the authentication ticket yourself and push the user roles in the “UserData
” property after authenticating the user. Also, you need to retrieve user roles from the same “UserData
” property in the authentication ticket and set it in the current User
property in the subsequent requests. This trick works, and many have done this already.
So, What is this Article About?
Well, this article assumes that you did use Forms authentication directly instead of ASP.NET Membership in your application for some good reasons. Consequently, you implemented Role based authorization as suggested by lots of articles on the web (like this one). But I tell you, you probably ended up doing an incorrect and incomplete implementation, and you might have problems in the near future.
This article is going to address the problems with the suggested implementation approaches, and provide you a correct, smart, and quick way of implementing Role based authorization in case you are not using ASP.NET Membership in your system. All you'll need is 5 minutes to implement this!
Please take a look at this article before you proceed, in case you are new to ASP.NET and wondering about Forms Authentication.
OK, So What is the Problem with the Suggested Approaches?
As was said already, the suggested approaches for implementing Role based authorization have some problems, and I realized those while trying to implement them in one of my ASP.NET applications. I did what was suggested in one of those articles, and found that the authorization was working fine. But, in order to fulfill a client request, I had to increase the cookie timeout
property in the <forms>
element and set it to “120” (120 minutes), and found that, the timeout value change didn't have any impact on the application. Exploring this, I was surprised to see that the system was never reading the increased value; rather, it was always reading “30”, the default value.
I was curious to investigate this issue and found another problem. I specified cookieless="UseUri"
in the <forms>
element, to test whether the Forms authentication worked (by writing authentication ticket in the request URL) if cookies are disabled in the client’s browser. Surprise again, now the system stopped authenticating the user!
Besides, I had a quick look at the authentication/authorization code (that was written to implement Role based authorization as suggested), and thought, why do I have to write all these codes? It should be fairly easy for anybody to implement it just by changing one or two lines of code.
So, I decided to write my own code, and share it with you!
How Easy Is It for You to Use my Implementation?
Well, I assume that you already have implemented Forms authentication in your application and configured stuff in the web.config. So, to implement Role based authorization, now you just need to do following three easy things, requiring a maximum of five minutes in total to implement.
- Add a reference to RoleBasedFormAuthentication.dll (which you can download from this article, along with the source code) in your web site/project.
- Instead of calling the following method after authenticating the user:
FormsAuthentication.RedirectFromLoginPage(userName,createPersistantCookie);
call the following method:
FormsAuthenticationUtil.RedirectFromLoginPage(userName,
commaSeperatedRoles, createPersistantCookie);
- Add the following code in the Global.asax file, or, change the code if it is already there:
protected void Application_AuthenticateRequest(Object sender,EventArgs e)
{
FormsAuthenticationUtil.AttachRolesToUser();
}
That’s it, you are done.
Curious? Here are the Details
I created my version of the authentication/authorization code and had overcome the three mentioned issues, as follows:
Solving the “timeout” Problem
While creating the FormsAuthenticationTicket
object, we need to provide five parameters. Take a look at the following method which creates the authentication ticket:
private static FormsAuthenticationTicket CreateAuthenticationTicket(string userName,
string commaSeperatedRoles, bool createPersistentCookie, string strCookiePath)
{
string cookiePath = strCookiePath == null ?
FormsAuthentication.FormsCookiePath : strCookiePath;
int expirationMinutes = GetCookieTimeoutValue();
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
userName,
DateTime.Now,
DateTime.Now.AddMinutes(expirationMinutes),
createPersistentCookie,
commaSeperatedRoles,
cookiePath);
return ticket;
}
Note the third parameter DateTime.Now.AddMinutes(expirationMinutes)
. Here, we expect the expirationMinutes
variable’s value to be read from the timeout
property in the <forms>
section. But, unfortunately, like the FormsAuthentication.FormsCookiePath
property (that reads the path configuration value specified in the <forms>
section), FormsAuthentication
or any other class does not give you any way to read the timeout
property value. I don't know why.
So, I had to implement and use the following method to read the timeout
property from web.config (if it is specified) and set the value while creating the FormsAuthenticationTicket
object.
private static int GetCookieTimeoutValue()
{
int timeout = 30;
XmlDocument webConfig = new XmlDocument();
webConfig.Load(HttpContext.Current.Server.MapPath("web.config"));
XmlNode node = webConfig.SelectSingleNode("/configuration/" +
"system.web/authentication/forms");
if (node != null && node.Attributes["timeout"] != null)
{
timeout = int.Parse(node.Attributes["timeout"].Value);
}
return timeout;
}
After doing this, the system was able to read the “timeout
” value from the web.config properly and set it in the authentication ticket object.
Solving the “cookieless” Problem
If the “cookieless
” property in the web.config is set to "UseUri
", or if for any reason the browser doesn't support cookies, or, if the browser has cookie support but disabled in the settings, the Forms authentication writes the authentication ticket in the URL and reads the ticket back on subsequent requests.
So, while we create the authentication ticket ourselves in order to implement Role based authorization, we need to implement the same logic, otherwise we will have problems. So, we need to determine whether we have to embed the ticket within a Cookie, or, we have to write the ticket to the URL based on the situation described above. The following code does this:
private static void SetAuthCookieMain(string userName, string commaSeperatedRoles,
bool createPersistentCookie, string strCookiePath)
{
FormsAuthenticationTicket ticket =
CreateAuthenticationTicket(userName, commaSeperatedRoles,
createPersistentCookie, strCookiePath);
string encrypetedTicket = FormsAuthentication.Encrypt(ticket);
if (!FormsAuthentication.CookiesSupported)
{
FormsAuthentication.SetAuthCookie(encrypetedTicket, false);
}
else
{
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
encrypetedTicket);
authCookie.Expires = ticket.Expiration;
HttpContext.Current.Response.Cookies.Add(authCookie);
}
}
The following piece of code does the main trick here:
if (!FormsAuthentication.CookiesSupported)
{
FormsAuthentication.SetAuthCookie(encrypetedTicket, false);
}
The FormsAuthentication.SetAuthCookie()
method may be a misleading one. As the name suggests, it seems to create the Forms authentication cookie with the authentication ticket. Yes, it does. But, if cookies are not supported in the browser, it sets the encrypted authorization ticket content into the URL. So now, if the browser doesn't support cookies, Forms authentication and Role based authorization will work fine for us.
Please note that after changing the code as above, we also need to modify the code where user roles are set on subsequent requests (in the Application_AuthenticateRequest()
event in Global.asax).
…
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = (id.Ticket);
if (!FormsAuthentication.CookiesSupported)
{
ticket = FormsAuthentication.Decrypt(id.Ticket.Name);
}
if (!string.IsNullOrEmpty(ticket.UserData))
{
string userData = ticket.UserData;
string[] roles = userData.Split(',');
HttpContext.Current.User =
new System.Security.Principal.GenericPrincipal(id, roles);
}
}
}
}
I just added the following piece of code after FormsAuthenticationTicket ticket = (id.Ticket);
.
if (!FormsAuthentication.CookiesSupported)
{
ticket = FormsAuthentication.Decrypt(id.Ticket.Name);
}
So, this was the solution to the “cookieless” problem.
Decoupling the Codes in a Reusable DLL
The golden principle of “Encapsulation” says that you should encapsulate your complexities to the outside world. So, why don't we encapsulate all this dirty nonsense code into a box? Why don't we stay clean?
Being inspired to follow this principle, I created a Class Library (“RoleBasedFormAuthentication
”) and moved the entire authentication and authorization related code there. I created a FormsAuthenticationUtil
class inside the class library, and implemented the following core reusable private
methods inside it:
Private Methods
private static FormsAuthenticationTicket CreateAuthenticationTicket(…)
private static void SetAuthCookieMain(…)
private static void RedirectFromLoginPageMain(…)
The above three are the core methods that are being used by the public
methods exposed to the outside world. The following are the public
methods (with their overloaded versions) implemented inside the class:
Public Methods
public static void RedirectFromLoginPage(…)
public static void SetAuthCookie(…)
public static void AttachRolesToUser()
These public
methods are being called by the client web application to implement Forms authentication and Role based authorization. Decoupling and implementing all authorization and authorization related logic inside the class library allows us to implement Role based authorization in our ASP.NET applications:
- In a small amount of time.
- In the correct way.
- In a cleaner and smarter way.
The Sample Project
Download the sample ASP.NET web site application (created using Visual Studio 2008, Framework 3.5) and unzip it (FormsAuthorization.zip) into a convenient location. Open the web site using Visual Studio, or, create an IIS site/virtual directory pointing to the web root folder of the sample web site. Assuming that you have created the IIS site/virtual directory, do the following to verify the authentication and Role based authorization along with the mentioned issues.
Testing Authorization
- Hit the following URL in the browser: http://localhost/FormsAuthorization/Admin/Default.aspx. The system will redirect you to the login page. Provide “Administrator/123” as the login credential and press “Login”. You will get a page where the “Hello Admin” message is displayed.
Hit the same URL again by logging out, or, opening a new browser window/tab. But, this time, provide “John/123” as the credential. The system will not let you access the page; rather, the login screen will remain there intact.
Looking at the web.config file of the web site, you will see that only the “Admin” role is allowed to access this URL and all other users are denied access. That is why John’s credential (who is a member of the “User” role) cannot access the URL that belongs to only the “Admin” role.
<location path="Admin">
<system.web>
<authorization>
<allow roles="Admin"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
- Hit the following URL in the browser: http://localhost/FormsAuthorization/User/Default.aspx. The system will redirect you to the login page. Provide “John/123” as the login credential and press “Login”. You will get a page where a “Hello John” message is displayed.
Hit the same URL again by logging out, or, opening a new browser window/tab. But, this time provide “Administrator /123” as the credential. The system will not let you access the page; rather, the login screen will remain there intact.
Looking at the web.config file of the web site, you will see that only the “User” role is allowed to access this URL and all other users are denied access. That is why Admin’s credential (who is a member of the “Admin” role) cannot access the URL that belongs to only the “User” role.
<location path="User">
<system.web>
<authorization>
<allow roles="User"/>
<deny users="*"/>
</authorization>
</system.web>
</location>
- Hit the following URL in the browser: http://localhost/FormsAuthorization/Public/Default.aspx. The system will display “Hello, this is a public page”. As you can understand, this is a public page and no credential is required to access this page.
Looking at the web.config file of the web site, you will see that all users are allowed to access this URL. So, no login credential is required to access it.
<location path="Public">
<system.web>
<authorization>
<allow users="*"/>
</authorization>
</system.web>
</location>
Testing the “timeout” Property
Testing the “cookieless” Property
- Change the “
cookieless
” property value and set it to “UseUri
” in the web.config.
<forms name="login" timeout="120"
loginUrl="Login.aspx" cookieless="UseUri"></forms>
- Hit the following URL in the browser: http://localhost/FormsAuthorization/Admin/Default.aspx and login using “Administrator/123” as the login credential. The system will log you in successfully.
- Take a look at the URL in the address bar. This should look something like the following:
http://localhost/FormsAuthorization/(F(Oz5JC7onSkVsmb6....))/Admin/Default.aspx
You can see that the authentication ticket has been encrypted and included in the URL (the actual URL should be a large one, and to save space, the remaining parts of the encrypted ticket in the URL has been omitted using some dots). This indicates that the system was able to write the authentication ticket in the URL and perform authentication and authorization correctly.
The sample web site “FormsAuthorization” uses the class library “RoleBasedFormsAuthentication.dll” to implement the authentication and Role based authorization. The source code for the class library is also available for download (RoleBasedFormsAuthentication.zip) in this article.
Conclusion
Despite the fact that Membership is a rocking stuff implemented in ASP.NET, the basic Forms authentication is not going to be eliminated at all and is going to be used over and over again. This article does not discuss about any “Rocket Science” here, and I just hope my effort would help you to implement a robust Forms authentication/authorization system and would save some of your precious time. I wish the commaSeperatedRoles
parameter (or something like it) will be included in the FormsAuthentication.RedirectFromLoginPage()
and other related methods in the future versions of the ASP.NET Framework.
Happy programming!
History
- May 30, 2009: First version
- Dec 3, 2009: Updated download files