Implementing Role-Based Security with GenericPrincipals and Forms Authentication - Stuart Gunter
in

dotnet.org.za

South African .NET Developer Portal

This Blog

Syndication

News


Get Firefox!
<!-- Begin Nedstat Basic code --> <!-- Title: StuartGunter --> <!-- URL: http://dotnet.org.za/stuartg/ --> <!-- End Nedstat Basic code -->

Stuart Gunter

There's too much!

Implementing Role-Based Security with GenericPrincipals and Forms Authentication

I've already posted this on SADeveloper if you want to read it there and download to source code.

 

Requirements

I did these examples using the following:

  • .NET Framework v1.1
  • Visual Studio .NET 2003
  • ASP.NET
  • C#
  • SQL Server 2000 (but you can just as easily use MS Access or other database technology)

 

 

Introduction

ASP.NET has a lot of powerful features that can make life a lot easier for software developers. A huge amount of benefit can be found in using some of the new authentication and authorisation features that ASP.NET has to offer. This article is targeted at developers that need to find a simple, yet powerful, way to authenticate users by making use of the .NET Framework.

 

I am relatively new to this myself, so please add comments and correct me if I go wrong. I decided to write this article because I needed to find a way to do this and battled to find any decent articles to explain it to me. Hopefully this will help someone who may be facing the same problem.

 

I’ll start off by explaining what it is and how it all works, and then I’ll give some sample code at the end. You’ll need to download the sample application if you want to see all the code, as I’ve only highlighted certain parts of the code in this article.

 

 

Authentication and Authorisation – What’s the difference?

Before I dive into explaining Forms Authentication, IPrincipal, IIdentity, GenericPrincipal, and all sorts of other funny things, I think it’s best to explain the difference between authentication and authorisation. These are two terms with which many people get confused, despite their relatively simple meanings.

 

Authentication is the process of validating a user against some repository (whether it be Active Directory, a private SQL Server database, or some other custom data store that holds user credentials) – “Who are you?

 

Authorisation is the process of allowing or denying access to a particular resource, object, etc. based on the individual user or their role membership – “What are you allowed to do/see?

 

Now that we’ve got the who and what out the way… let’s move onto the how!

 

 

What is Forms Authentication and how does it work?

Forms Authentication provides the ability to force a user to enter a username and password (or whatever credentials you wish) to gain access to a particular ASP.NET application. Security is enforced by redirecting a user to the login page of the application if they have not been authenticated.

 

ASP.NET provides four different options for authentication: Windows, Forms, Passport, None. Please note that “None” can either mean that no authentication is required, or that it is being controlled by some form of custom authentication. I won’t go into explaining what the other options are, but assume you’ll read up on them if you’re interested. Forms Authentication is probably your best option when you don’t have the option of using Windows Authentication, but you need some way to collect user credentials within your application.

 

As I mentioned earlier, Forms Authentication will redirect a user to the login page for authentication. If the application authenticates the logon request, a cookie is issued that stores the credentials or some other custom information to retrieve the client identity. After authentication has occurred the cookie is sent in the request headers, thus it is unnecessary to authenticate the user for subsequent requests.

 

To the best of my knowledge, it is possible to use Forms Authentication without using cookies. The initial release of .NET did not support this, but the new version does (I could be wrong on this… I haven’t tried it myself, but that’s what the documentation seems to point towards). In this case, you will need to design your own custom way of recognising authenticated users. This is quite dangerous if not planned properly. You must make sure that it isn’t easy for a user to enter a value into the querystring to simulate being logged in.

 

You should consider using Forms Authentication when you are validating users that do not all have Windows accounts, you are running an application over the Internet, you need to support any operating system or browser, or you need to validate against a custom data store.

 

 

IIdentity and IPrincipal – What are they?

In order to fully understand the value that can found in using the classes that implement these interfaces, you need to understand what the interfaces themselves define.

 

IIdentity is a very simple interface that defines only three members (all of them properties):

  • AuthenticationType – returns a string to indicate the type of authentication used to validate the user.
  • IsAuthenticated – returns a bool to indicate whether the user has been authenticated.
  • Name – returns a string value representing the user’s name.

 

IPrincipal defines only two members:

  • Identity – a property that returns the IIdentity object associated with the principal.
  • IsInRole – returns a bool to indicate whether the user is in the role specified in the parameter.

 

As stated in MSDN “An identity object represents the user on whose behalf the code is running”. A principal object extends this functionality by representing the security context of the user (exposed through the Identity property) as well as their role membership.

 

In simple terms: an identity object is used primarily for authentication, and a principal object is extended to provide additional functionality for authorisation.

 

 

What is a GenericPrincipal and why would I use one?

A GenericPrincipal is a very simple implementation of the IPrincipal interface. It is very useful when you need a principal object that is not bound to a Windows domain (as is WindowsPrincipal). Within your application you may need to implement some form of authorisation. Perhaps you need to hide certain controls on a Web Form depending on whether the user is in the “Admin” role or not. This can be very easily achieved by using the GenericPrincipal class.

 

Later in this article I will show you how to retrieve a list of roles from a SQL Server database, and store them in the principal object for the currently logged on user. From then on, you can just use the current context to do your authorisation.

 

 

Why use both?

Now you may be thinking, “I can use Forms Authentication without a GenericPrincipal. So why should I use one?”

 

Well, there’s a relatively simple answer to this question. Forms Authentication only covers one aspect of your security solution… authentication! So what are you using to do authorisation? Now that you’ve let the person into your system… how are you going to control what they see and do? That’s where the GenericPrincipal comes in. By using this class you’re able to maintain the list of roles assigned to the user and perform authorisation against those roles within your application.

 

 

Extending the GenericPrincipal

As I mentioned earlier, a GenericPrincipal is a very simple implementation of the IPrincipal interface. It exposes a very limited set of members for interacting with the class. You may have a need for more members than the GenericPrincipal can offer. If this is the case you have a number of options, but I’ll just explain one of them: creating your own class that inherits from GenericPrincipal.

 

I’ve created a very simple class that extends the functionality of that provided by GenericPrincipal. IPrincipal defines a method called IsInRole that checks whether the user is in the role supplied as a parameter. All I’ve done here is created an additional method called IsInAnyRole that checks if the user is in any of the roles supplied to the method. This allows you to call IsInAnyRole instead of calling the IsInRole method several times and OR-ing the results.

 

Instead of doing this:

 

if (userPrincipal.IsInRole(“Admin”) || userPrincipal.IsInRole(“Manager”))

 

you can do this:

 

string[] roles = new string[] {“Admin”, “Manager”};

if (userPrincipal.IsInAnyRole(roles))

 

or this:

 

if (userPrincipal.IsInAnyRole(“Admin”, “Manager”))

 

You could add other methods that allow hierarchical roles to be used. This will only work if your roles are designed for it (e.g. “Guest” is less than “User” is less than “Admin”). You could then check if a user is greater than or equal to “User” (which applies to all “Users” and “Admins”).

 

Another option you have is to create a new implementation class of IPrincipal, but I would recommend that you rather inherit from GenericPrincipal. You’ll then be making use of an existing class that works, and just extending it to provide any custom members.

 

Please note that you can also extend the GenericIdentity class (although I haven’t covered that here). Remember that the identity object represents the user; so extending this class to allow additional properties, such as telephone number, is quite possible. If you need to add members that affect authorisation then it is best to extend the principal classes, as they are responsible for this functionality.

 

 

What about web.config?

I guess you’re wondering how this affects your web.config file. Well, there are only two elements in the config file that are affected by this. As you can probably guess, they’re called “authentication” and “authorization”.

 

The authentication element has an attribute named “mode”. As documented in the web.config file, you are able to set this to one of four values: Windows, Passport, Forms, None. You can go ahead and set this to Forms, as we are using Forms Authentication. You’ll need to add a sub-element called “forms” with a number of attributes set. Below is what your authentication element should look like:

 

<authentication mode="Forms">

<forms name="adAuthCookie" loginUrl="Logon.aspx" protection="All" timeout="60" path="/" />

authentication>

 

For the sake of simplicity, I’ve just outlined the purpose of each of these attributes in a table. I assume you’ll do a little research if you’re not sure of anything.

 

Attribute

Purpose

name

Set this to the name of your cookie. Default value is “.ASPXAUTH” and a unique name is required if multiple applications are running on the same web server.

loginUrl

Set this to the login page of your application. This is where unauthenticated users will be redirected when no valid authentication cookie is found.

protection

“All” is the default value, and indicates that both data validation and encryption are used to secure the cookie. This is the recommended option.

timeout

Number of minutes for timeout.

path

Specifies the path for cookies that the application issues. Default is a slash “/” due to case sensitivity issues with some browsers.

 

Now we need to update the authorization element. You can add sub-elements here to control who has access to the application. We need to set one sub-element for our application to work. This is what the authorisation element now looks like:

 

<authorization>

<deny users="?" /> -- Deny anonymous users -->

authorization>

 

The way ASP.NET uses these settings is very important. It will go through the list until it finds the first setting that applies to the user. If we had put the deny element below an element, anonymous users would be granted access to our application. You can allow or deny users and roles by setting these values in the config file. This is documented in the web.config, so I assume you’ll read it.

 

 

Show me how!

I’ll take you through the code step-by-step and show you what you need to do to implement this in your application. From here on I’ll put everything into a sequence that can be followed, similar to the Microsoft HOW TO guides. Please note that I haven’t put much time into getting the database perfect because that’s not my focus for this example. I tried my best, but with limited time I can’t make it perfect.

 

Step 1:        Create a Web Application and Logon.aspx Page

  1. Open Visual Studio .NET and create a new ASP.NET Web Application using C#.
  2. Rename WebForm1.aspx to Logon.aspx
  3. Add the following controls to the page:

Type

ID

Text

Label

-

UserName:

Label

-

Password:

Label

lblError

TextBox

txtUserName

TextBox

txtPassword

Button

btnLogon

Log On

 

  1. Set the TextMode property of the password TextBox to Password.
  2. Add the following using statements to your code-behind page:

using System.Web.Security;

using System.Data.SqlClient;

 

  1. Double-click the Log On button and add the following code to its click event handler:

try

{

       // Get username and password

       string userName = txtUserName.Text;

       string password = txtPassword.Text;

      

       // Create connection and command and get data

       SqlConnection sqlConn = new SqlConnection("server=(local);database=pubs;uid=sa;pwd=password");

       SqlCommand sqlCmd = new SqlCommand("GetUserRoles", sqlConn);

       sqlCmd.CommandType = CommandType.StoredProcedure;

       sqlCmd.Parameters.Add("@username", userName);

       sqlCmd.Parameters.Add("@password", password);

       sqlConn.Open();

       SqlDataReader sqlDr = sqlCmd.ExecuteReader();

 

       // Iterate through resultset and create a pipe-delimited string of role memberships

       string roles = "";

       while (sqlDr.Read())

              roles += sqlDr.GetString(0) + "|";

 

       // If roles found, then user is valid

       if (roles.Length > 0)

       {

              FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1,

                     userName, DateTime.Now, DateTime.Now.AddMinutes(30), false, roles);

 

              // Encrypt the ticket

              string encryptedTicket = FormsAuthentication.Encrypt(authTicket);

 

              // Create a cookie and add the encrypted ticket as data

              HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName,

encryptedTicket);

 

              // Add the cookie to the outgoing cookies collection

              Response.Cookies.Add(authCookie);

 

              // Redirect the user to the originally requested page

              Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, false));

       }

       else

       {

              lblError.Text = "Authentication failed. Check your UserName and Password.";

       }

}

catch (Exception ex)

{

       lblError.Text = "Error Authenticating: " + ex.Message;

}

 

Step 2:        Create a default.aspx Web Form

  1. Add a new Web Form to your project and call it default.aspx.
  2. Right-click the form in Solution Explorer and click Set As Start Page.
  3. Double-click the form and add the following code to the Page_Load event handler:

 

CustomPrincipal principal = (CustomPrincipal)HttpContext.Current.User;

string[] roles = new string[] {"Admin", "Manager"};

Response.Write("UserName: " + principal.Identity.Name + "
");

 

if (principal.IsInRole("Admin"))

       Response.Write("Admin
");

else

       Response.Write("Admin
");

 

if (principal.IsInRole("Manager"))

       Response.Write("Manager
");

else

       Response.Write("Manager
");

             

if (principal.IsInRole("User"))

       Response.Write("User
");

else

       Response.Write("User
");

 

Step 3:        Modify the Web.config File

  1. Open the Web.config file and go to the element.
  2. Replace the definition in your Web.config with the following code:

 

<authentication mode="Forms">

       <forms name="adAuthCookie" loginUrl="Logon.aspx" protection="All" timeout="60" path="/" />

authentication>

 

  1. Replace the definition of your element with the following code:

 

<authorization>

<deny users="?" />

authorization>

 

Step 4:        Create a CustomPrincipal class

  1. Add a new Class to your project and call it CustomPrincipal.cs.
  2. You will need to add a using statement to your class to inherit from GenericPrincipal.

 

using System.Security.Principal;

 

  1. Define your CustomPrincipal class as follows:

 

public class CustomPrincipal : GenericPrincipal

{

       public CustomPrincipal(IIdentity identity, string[] roles) : base(identity, roles)

       {

       }

 

       public bool IsInAnyRole(params string[] roles)

       {

              foreach (string role in roles)

              {

                     if (this.IsInRole(role))

                           return true;

              }

 

              return false;

       }

}

 

  1. You can add additional members to this class if you would like to. If you remove the IsInAnyRole method, be sure to update the default.aspx page accordingly.

 

Step 5:        Modify the Global.asax File

  1. Open the Code View of your Global.asax file and add the following using statement:

 

using System.Security.Principal;

 

  1. Now add the following code to your Application_AuthenticateRequest event handler:

 

// Extract the forms authentication cookie

string cookieName = FormsAuthentication.FormsCookieName;

HttpCookie authCookie = Context.Request.Cookies[cookieName];

 

if (authCookie == null)

{

       // There is no authentication cookie

       return;

}

 

FormsAuthenticationTicket authTicket = null;

 

try

{

       authTicket = FormsAuthentication.Decrypt(authCookie.Value);

}

catch (Exception)

{

       // Log exception details (omitted for simplicity)

       return;

}

 

if (authTicket == null)

{

       // Failed to decrypt

       return;

}

 

// When the ticket was created, the UserData property was assigned a pipe-delimited string of roles

string[] roles = authTicket.UserData.Split(new char[] {'|'});

 

// Create a GenericIdentity object

GenericIdentity id = new GenericIdentity(authTicket.Name, "FormsAuthentication");

                    

// Now we use our CustomPrincipal class that will flow throughout the request

CustomPrincipal principal = new CustomPrincipal(id, roles);

 

// Attach the new Principal object to the current HttpContext.User object

Context.User = principal;

 

Step 6:        Create the Database

  1. Execute the scripts to create the tables and stored procedure against the pubs database.

 

 

References

I have used a fair amount of material from other articles off various websites. These are links to articles that I’ve either borrowed information from, or just useful references.

 

I used the MSDN Library for most of the information in this article. There’s so much in there, and it’s really worthwhile looking there before going too much further. But for code examples, most C# or ASP.NET community sites are worthwhile. These are my favourites (in no particular order):

Published May 07 2004, 02:28 PM by stuartg
Filed under: , ,

Comments

 

Thea Burger said:

I am busy implementing Forms authentication with Role-based security where all my aspx pages inherits from a base class that checks the permission. There really aren't a lot of examples of on how to do this without making a call to the db each time in global.asax. Did it a bit different, but your article was a good example. Thanks.
June 1, 2004 3:50 PM
 

StuartGunter said:

No prob... I've seen many places that this can be improved, btw! I'd recommend you check out "Building Secure ASP.NET Applications" on the MSDN Patterns & Practices site (http://msdn.microsoft.com/practices/)

It's an invaluable resource for any .NET developer / designer!!!
June 1, 2004 4:45 PM
 

Thea Burger said:

Yes, I know of it. I look at some more options there. :)
June 2, 2004 2:15 PM
 

tomahawk said:

i did not get the whole part in the default.aspx

like

if (principal.IsInRole("Admin"))
Response.Write("Admin");
else
Response.Write("Admin");
either way it is writing"admin"


August 3, 2004 3:54 PM
 

Dairy said:

Needs idea to implement Security/User Authentication in MS-Access 2000. if yes Pls, PM me

i need to develop MS-Access application with User Auth., and also i need to set the roles, for eg., Admin - can view all the forms, Finance guy can view only Finance Menus...etc.,
January 21, 2005 2:41 PM
 

Moutan said:

This article was really helpful

in making me understand the role based

security system of .NET

October 1, 2007 11:33 AM
 

ashutosh said:

gd1

October 7, 2007 9:25 PM
 

Waqas Aziz said:

Nice Work Buddy

You did a lot of effort and i realy appreciate it

Thanks for your article as without it i lwould never make is happen

March 22, 2008 10:55 AM
 

yonkster said:

I have follow all your instructions, but when i use Response.Redirect("default.aspx") it cause ThreadAbortException. Then after i review support.microsoft.com/default.aspx, it's tell me to put false option in second parameter of Response.Redirect. so it's become Response.Redirect("default.aspx",false). The ThreadAbortException doesn't occur anymore, but still i cannot enter to default.aspx. Can you tell me the reason?? Thanks...

May 15, 2008 12:14 PM
 

gbeye said:

try giving the response.redirect statement outside the try catch block

May 17, 2008 6:15 AM