Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / ASP.NET

Forms Authentication and Role based Authorization: A Quicker, Simpler, and Correct Approach

4.78/5 (120 votes)
3 Dec 2009CPOL11 min read 388.7K   12.6K  
This article describes a correct and smarter way of implementing Role based authorization with Forms authentication in ASP.NET.

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:
    C#
    FormsAuthentication.RedirectFromLoginPage(userName,createPersistantCookie);

    call the following method:

    C#
    FormsAuthenticationUtil.RedirectFromLoginPage(userName, 
                            commaSeperatedRoles, createPersistantCookie);
  • Add the following code in the Global.asax file, or, change the code if it is already there:
    C#
    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:

C#
/// <summary> 
/// Creates and returns the Forms authentication ticket
/// </summary>
/// <param name="userName">User name</param>
/// <param name="commaSeperatedRoles">Comma separated roles for the users</param>
/// <param name="createPersistentCookie">True or false
///        whether to create persistant cookie</param>
/// <param name="strCookiePath">Path for which the authentication ticket is valid</param>
private static FormsAuthenticationTicket CreateAuthenticationTicket(string userName, 
        string commaSeperatedRoles, bool createPersistentCookie, string strCookiePath)
{
    string cookiePath = strCookiePath == null ? 
      FormsAuthentication.FormsCookiePath : strCookiePath;
    //Determine the cookie timeout value from web.config if specified
    int expirationMinutes = GetCookieTimeoutValue();
    //Create the authentication ticket
    FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 
    1, //A dummy ticket version
    userName, //User name for whom the ticket is issued
    DateTime.Now, //Current date and time
    DateTime.Now.AddMinutes(expirationMinutes), //Expiration date and time
    createPersistentCookie, //Whether to persist cookie on client side. If true,
    //The authentication ticket will be issued for new sessions from the same client
    //PC
    commaSeperatedRoles, //Comma separated user roles
    cookiePath); //Path cookie valid for
    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.

C#
/// <summary>
/// Retrieves cookie timeout value in the <forms></forms>
/// section in the web.config file as this
/// value is not accessible via the FormsAuthentication or any other built in class
/// </summary>
/// <returns></returns>
private static int GetCookieTimeoutValue()
{
    int timeout = 30; //Default timeout is 30 minutes
    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:

C#
/// <summary>
/// Creates Forms authentication ticket and writes it in URL or embeds it within Cookie
/// </summary>
/// <param name="userName">User name</param>
/// <param name="commaSeperatedRoles">Comma separated roles for the users</param>
/// <param name="createPersistentCookie">True or false whether
///          to create persistant cookie</param>
/// <param name="strCookiePath">Path for which
///         the authentication ticket is valid</param>
private static void 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:

C#
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).

C#
/// <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);.

C#
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

C#
/// <summary> 
/// Creates and returns the Forms authentication ticket
/// </summary>
private static FormsAuthenticationTicket CreateAuthenticationTicket(…)

/// <summary>
/// Creates a Forms authentication ticket using the private
/// method CreateAuthenticationTicket() and writes
/// it in URL or embeds it within Cookie
/// </summary> 
private static void 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>
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

C#
/// <summary>
/// Creates Forms authentication ticket and redirects
/// to the originally requested page. Uses the
/// RedirectFromLoginPageMain() private method
/// </summary>
public static void RedirectFromLoginPage(…)

/// <summary>
/// Creates a Forms authentication ticket and writes it
/// in URL or embeds it within Cookie. Uses the 
/// SetAuthCookieMain() private method
/// </summary>
public static void 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>
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.

    XML
    <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.

    XML
    <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.

    XML
    <location path="Public">
        <system.web>
            <authorization>
                <allow users="*"/>
            </authorization>
        </system.web>
    </location>

Testing the “timeout” Property

  • Change the “timeout” property value and set it to “1” in the web.config file.
    XML
    <forms name="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.
    XML
    <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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)