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:

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.

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:

///<summary>/// Creates Forms authentication ticket and writes it in URL or embeds it within Cookie
///</summary>///<paramname="userName">User name</param>///<paramname="commaSeperatedRoles">Comma separated roles for the users</param>///<paramname="createPersistentCookie">True or false whether
/// to create persistant cookie</param>///<paramname="strCookiePath">Path for which
/// the authentication ticket is valid</param>privatestaticvoid SetAuthCookieMain(string userName, string commaSeperatedRoles,
bool createPersistentCookie, string strCookiePath)
{
FormsAuthenticationTicket ticket =
CreateAuthenticationTicket(userName, commaSeperatedRoles,
createPersistentCookie, strCookiePath);
//Encrypt the authentication ticket
string encrypetedTicket = FormsAuthentication.Encrypt(ticket);
if (!FormsAuthentication.CookiesSupported)
{
//If the authentication ticket is specified not to use cookie, set it in the URL
FormsAuthentication.SetAuthCookie(encrypetedTicket, false);
}
else
{
//If the authentication ticket is specified to use a cookie,
//wrap it within a cookie.
//The default cookie name is .ASPXAUTH if not specified
//in the <forms> element in web.config
HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,
encrypetedTicket);
//Set the cookie's expiration time to the tickets expiration time
authCookie.Expires = ticket.Expiration;
//Set the cookie in the Response
HttpContext.Current.Response.Cookies.Add(authCookie);
}
}

The following piece of code does the main trick here:

if (!FormsAuthentication.CookiesSupported)
{
//If the authentication ticket is specified not to use cookie, set it in the URL
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).

///<summary>/// Adds roles to the current User in HttpContext
/// after forms authentication authenticates the user
/// so that, the authorization mechanism can authorize
/// user based on the groups/roles of the user
///</summary>…
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)
{
//If cookie is not supported for forms authentication, then the
//authentication ticket is stored in the URL, which is encrypted.
//So, decrypt it
ticket = FormsAuthentication.Decrypt(id.Ticket.Name);
}
// Get the stored user-data, in this case, user roles
if (!string.IsNullOrEmpty(ticket.UserData))
{
string userData = ticket.UserData;
string[] roles = userData.Split(',');
//Roles were put in the UserData property in the authentication ticket
//while creating it
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)
{
//If cookie is not supported for forms authentication, then the
//authentication ticket is stored in the URL, which is encrypted.
//So, decrypt it
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

///<summary>/// Creates and returns the Forms authentication ticket
///</summary>privatestatic FormsAuthenticationTicket CreateAuthenticationTicket(…)
///<summary>/// Creates a Forms authentication ticket using the private
/// method CreateAuthenticationTicket() and writes
/// it in URL or embeds it within Cookie
///</summary>privatestaticvoid SetAuthCookieMain(…)
///<summary>/// Creates a Forms authentication ticket and sets it within URL or Cookie
/// using the SetAuthCookieMain() private method, and redirects
/// to the originally requested page
///</summary>privatestaticvoid 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

///<summary>/// Creates Forms authentication ticket and redirects
/// to the originally requested page. Uses the
/// RedirectFromLoginPageMain() private method
///</summary>publicstaticvoid RedirectFromLoginPage(…)
///<summary>/// Creates a Forms authentication ticket and writes it
/// in URL or embeds it within Cookie. Uses the
/// SetAuthCookieMain() private method
///</summary>publicstaticvoid SetAuthCookie(…)
///<summary>/// Adds roles to the current User in HttpContext
/// after forms authentication authenticates the user
/// so that, the authorization mechanism can authorize
/// user based on the groups/roles of the user
///</summary>publicstaticvoid 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.

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.

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.

Testing the “timeout” Property

Change the “timeout” property value and set it to “1” in the web.config file.

<formsname="login"timeout="1"loginUrl="Login.aspx"></forms>

Hit the following URL in the browser: http://localhost/FormsAuthorization/Admin/Default.aspx and login using “Administrator/123” as the login credential.

Don't do anything for the next 1+ minute, and when a minute has passed, refresh the page. The system will redirect you to the login page, because, the authentication cookie has expired in the mean time. This indicates that the system can read and apply the “timeout” property correctly from the web.config.

Testing the “cookieless” Property

Change the “cookieless” property value and set it to “UseUri” in the web.config.

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.

Share

About the Author

Sorry for not being able to contribute to CodeProject these days. I'be been busy with DropCue, which is not just another "to-do-list" or calendar management app, but an app to manage all of your "personal aspects" in one single place in such a simple, easy and innovative approach that no other system offers.

Also I added the appropriate method to File global.asax and use AuthenticateUser method like in your sample project.
It forces the user to log in. But every user have access to every location - it's not depend on their role at all. When I debug the Home class, I can see User is assigned only the role of the User - but Home should be for Admin only..