Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Quick ASP.NET Security


Quick ASP.NET Security

Download the code for this issue

Code freeze is scheduled to take place in a week, and lurking in the back of your mind is that chapter of your design spec that talks about limiting user access to web forms based on business rules. The problem is, you've been so focused on functionality that you haven't started coding this yet, other than maybe adding some Windows groups to the web.config file and turning off anonymous access to the web site in IIS.

Controlling user access is an essential part of ASP.NET-based web forms. You can specify various Windows groups (sometimes referred to as NT groups or roles) in the web.config file. But even though John the editor is a member of the MyAppUsers group and has access to the app, he shouldn't be allowed to access the Salary Maintenance form — only managers should be able to update salaries. It's best to implement security right from the start of development, but on a recent project, I needed to add security just a few weeks before a major deadline. I already had it worked out in my head, and I implemented it in a single day.

This article will show you how to easily create application roles of your own that you can map to real Windows groups in the web.config file, and how to enforce these roles throughout your application by simply defining an attribute for each page or adding a few lines of code. At the same time, you will get hands-on experience with inheritance, reflection, and attributes — one of those .NET features you may have heard about but haven't used yet. All listings for this example are shown in C#; however, the same listings are available in VB.NET in the source-code download available online.

This article won't cover the same information presented in the .NET Framework documentation on ASP.NET security, so you might want to brush up on the basics if you are not already familiar with them. See the .NET Framework SDK chapter titled "ASP.NET Web Application Security" on MSDN or in the Visual Studio .NET documentation. I focus specifically on applications using Windows Authentication, but my model could be applied to Forms Authentication as well.

Setting up web.config

For the sake of example, let's assume that your application is called "Maya" and must define four business roles: Administrator, Manager, Human Resources (HR), and Editor. Editors may view and edit articles, but they may not view personnel records, much less edit them. HR may view personnel records, but only Managers may make changes to salary. Administrators can do anything. I've defined an enumeration, MayaRole, in Listing 1 to represent this. I've also added a special value, ALL, which I will later use to refer to all application roles.

If you are using integrated Windows Authentication in your application, it is likely that your web.config file contains an entry for <allow roles=...>, and if it doesn't, it probably should (see ASP.NET Authorization under ASP.NET Web Application Security in the documentation). This is a great way to ensure limited access to the application, but it doesn't describe who can access what forms and what they are allowed to do on said forms. We will add our own application settings to the web.config file as in Listing 2. Note that <appSettings> goes inside the <configuration> section of the web.config file; there is more on this in the documentation. These settings act as a map from Windows groups to our application roles. You could simply hard-code certain groups in the application code itself, but creating this mapping has some great benefits. For example, you may want your testers to have the Manager role on the test system, but not on the production system — both systems are the same domain. In production, you could assign the MyDomain\Managers group to the Manager role, but use MyDomain \Test_Managers in the test server's web.config file. This way, you don't have to make the testers into members of the MyDomain\Managers group in order for them to test the Manager role. The web.config file for a test web site might look like Listing 3.

If you are not using Windows authentication, you can still use a similar model. For example, if you are checking for a username and password in a database table, you could add an additional table of groups for each user, which can be processed in a similar way to how we will process the user's Windows groups to determine which application roles apply to the user.

Create the Security Class

Now we need to read the information from the web.config file in order to determine which application roles the current user has. Let's create a class called MayaSecurity for this purpose. In web forms, the System.Web.UI.Page class has a public property User, which is an instance of IPrincipal. We will pass this object to the constructor of MayaSecurity in order to check the user's roles. Role checking is performed by examining each of the Windows groups listed in the web.config file for each role we have defined. I've set it up so that this may be a comma-separated list of groups, which is handy if you work on multiple domains; for example, with a laptop.

We check permissions by looping through all possible application roles, and for each role, we loop through the comma-separated list of Windows groups, calling the IPrincipal.IsInRole method. If IsInRole returns True for any of the groups listed for a given role, then we add that role to an ArrayList of roles. I keep a list of roles so that a user may be a member of multiple application roles, which makes sense because a user may also be a member of multiple Windows groups. Notice that I loop through the roles by using reflection to get all of the possible values of the MayaRole enumeration.

Listing 4 shows the MayaSecurity class, which includes the CheckRoles method. I also added a method called IsMemberOf. This method takes any MayaRole as a parameter and returns True if the user is a member of that role. Similarly, the property IsAdmin returns True if the user is a member of MayaRole.Admin, and IsUser returns True if the user is a member of at least one role.

Note that you will need to have an <authorization> section in your web.config file specifying the Windows groups that are allowed to access the application at all. This would normally include all of the groups that you specify in the custom section in Listings 2 and 3. If you do not have this section, then the authentication information will not be passed in, and calls to IPrincipal.IsInRole will always return False; see Listing 5.

Inheriting from System.Web.UI.Page

Now it's time to see how we can put the MayaSecurity class to work. You may have noticed in your code-behinds that all of your web forms inherit from the System.Web.UI.Page class by default. You can create your own class that inherits from Page called MayaPage. Derive your web forms from MayaPage instead of the default. We're going to add code to MayaPage that will execute for every page that inherits from it, without having to repeat that code over and over again. The ability to do this is a direct result of the benefits of object inheritance and the ASP.NET architecture. The Page lifecycle includes initialization, and this is where our code will go.

You can learn all about the lifecycle of a web form in a couple of different sections of the .NET Framework documentation — if you are interested, I recommend checking "control execution lifecycle" (a Page is really a specialized control) and "about HttpApplication class" (Page processing takes place in "[The handler is executed.]") in the index.

We are going to override Page's OnInit method in our MayaPage class and load the security information there. First, we will check to see if it is cached in the Session object, and if not, create it and cache it there for improved performance on future pages. Because we are overriding OnInit, we also need to call the base class's implementation of OnInit to ensure proper page processing (see Listing 6). As a side note, overriding methods that are called during the page-execution lifecycle is also a great way to put custom tracing or logging in your application.

Creating Custom Attributes

Notice the call to AuthenticateUserForPage. This method will compare the user's security profile with that of the current page by examining the page's attributes. We will define custom attribute classes called MayaViewRolesAttribute and MayaEditRolesAttribute. They will take a list of roles as parameters, and then we can specify who may view or edit a particular page with one or two lines of code, as in Listing 7. Note that VB.NET does not allow custom attributes to accept ParamArray in the constructor, so in the VB version the custom attributes take only one MayaRole as a parameter, and you may specify each attribute multiple times in order to specify multiple roles.

The two attributes, MayaViewRolesAttribute and MayaEditRolesAttribute, have essentially the same functionality, so we can create an abstract MayaRolesAttribute class containing the code that each of these inherits from. The class will simply keep an ArrayList of the roles that were specified in the attribute constructor. We will add more code to MayaPage that will use reflection to examine the current page's attributes and determine if the user has the roles necessary to view or edit the page. With reflection, we can simply ask for all attributes of type MayaRolesAttribute, and the results will contain the two inherited attributes we created. You could extend this to suit your project's needs by adding, for example, MayaDeleteRolesAttribute, if your project differentiated between edit and delete permissions.

See Listing 8 for the implementation of the attribute classes. AttributeUsage specifies how the new attributes can be used: They apply to classes (as opposed to methods, etc.), multiples are not allowed (in the C# example), and derived classes inherit the attributes. See "attributes tutorial" in the .NET Framework documentation for more information. The constructor takes 0 or more MayaRole values and inserts them into the ArrayList (or just one MayaRole in the VB example). Three methods are also provided for checking the roles.

IsRoleSpecified returns True if the specified role is in the list of roles. Note that when I defined the MayaRole enumeration, I added a special role called ALL. This is where I make use of it — instead of checking only for the specific role, I also return True if MayaRole.ALL has been specified. This makes it easy to provide access to a page to everyone simply by specifying the attribute "[MayaViewRoles(MayaRole.ALL)]." It should be noted that the earlier code will also look for RoleALL in the web.config file, but that shouldn't cause any harm.

IsAnyRoleSpecified takes a list of roles and returns True if at least one of them is listed in the attribute. Finally, a second implementation of IsAnyRoleSpecified takes a MayaSecurity object as a parameter and makes the check against its internal ArrayList of roles (which it can access because it was defined as "internal/Friend"). MayaViewRolesAttribute and MayaEditRolesAttribute inherit from the abstract class and only need to delegate their constructors to the abstract class's constructor.

Using Reflection

Now that the custom attributes are defined, we can use reflection to check them in the AuthenticateUserForPage method of the MayaPage class. Recall that all of our secured pages will derive from MayaPage. When AuthenticateUserForPage is called from the overridden OnInit method, the code can use reflection to check its own attributes. It uses the this.GetType method to get an instance of the page type. If the SalaryMaintenance page is loading, then this.GetType will return the type of the SalaryMaintenance class compiled from SalarayMaintenance.aspx, probably something like SalaryMaintenance_aspx, as opposed to the MayaPage class, where this code is located.

See Listing 9 for the implementation. The GetCustomAttributes method is flexible in that we can specify a type and only retrieve attributes of that type. By passing the type of MayaRolesAttribute, GetCustomAttributes will return all attributes defined that are valid MayaRolesAttribute instances including MayaViewRolesAttribute and MayaEditRolesAttribute. For each attribute found, we call the IsAnyRoleSpecified method passing in our MayaSecurity object to determine if the current user has a role that the attribute specifies. I also added a variable called mayaAttributesFound, which I use to determine if there aren't any MayaRolesAttributes defined. If there aren't, I throw an exception because we should define attributes for each page. You may want to handle this differently.

Enforcing the Rules

The last thing that AuthenticateUserForPage does is ensure that the user may view the page. If the user may not view the page, it throws an exception stating so. This will abort the rest of the page processing and show an ugly error message to the user. I would recommend handling this with either a custom error page or a Response.Redirect to a page with a friendlier message. In any case, you are now guaranteed that the user will not be able to access the page unless the user's application roles match the roles required for the page as defined in the page attributes! You get that by adding attributes to the page's code-behind.

If you have pages where users can both view and edit information, and you need to allow some users to view and others to view and edit, then you will need to add some code to the page itself. The attributes will ensure that only users who can at least view the page will be able to access it, but you need to stop users from editing a page if they don't have permission. Fortunately, the work that we have so far makes this very simple. In your code-behind, you can simply check MayEdit. Perhaps the page has a view mode and an edit mode, and the user normally would press an Edit button in order to switch to edit mode. You could hide or disable this button if MayEdit is False. If there is an Update button to save changes to the database, you could check MayEdit in the button click event, and return an error message if MayEdit is not True. MayEdit, as well as MayView, are available to all of your pages that inherit from MayaPage, just like the other public Page properties such as User, Request, and so on. See Listing 10 (available online) for examples.

This model is very flexible, and you can use it to create highly customized security logic. For example, you might have a web form for managing a software project schedule where most users have view and edit permissions, but marketing personnel are not allowed to edit the project completion date field. We can add a few lines of code to the MayaPage class to expose methods of the MayaSecurity object, such as IsAdmin and IsMemberOf. On the project schedule form, you could check IsMemberOf for the marketing role and disable the completion date field. Adding the code in Listing 11 (available online) to MayaPage will expose these methods to your pages, and the code in Listing 12 (available online) shows how IsMemberOf could be used.

VB programmers will notice that I use the OrElse and AndAlso keywords — they are similar to Or and And except they do not evaluate the second part of the expression if they do not need to. They equate to || and && in C#, versus | and &. In traditional VB, the statement "If (myObj Is Nothing) Or (Len(myObj.Name) = 0) Then" will raise an error if myObj is Nothing because myObj.Name cannot be evaluated. However, "If (myObj Is Nothing) OrElse (Len(myObj.Name) = 0) Then" will not fail because (Len(myObject.Name) = 0) will not be evaluated if (myObj Is Nothing) evaluates to True.

Deployment Considerations

In this example, I cache the MayaSecurity object in the Session. If you are using Windows integrated authentication, then calls to IPrincipal.IsInRole may translate into calls to an Active Directory server in order to check the user's roles. In a healthy network, this should not be an issue, but problems in the network could lead to performance issues if these calls take a relatively long time for some reason. You could use a network monitor to see where network traffic is going while a page request is being serviced. Also note that HttpApplication will also need to check roles during the authorization phase to see if a user is allowed to access the site at all.

Another caveat — if you cache MayaSecurity and then the user's Windows groups are changed, the changes won't take effect until a new Session is created. This may or not be a problem in your organization. If it is, caching the object in the Session may not be an option, and you will need to recreate it on every page OnInit.

I've shown you how to create a flexible security model that you can easily integrate into any ASP.NET application. This is a great example of how leveraging object inheritance, attributes, reflection, and .NET's built-in security features allow us to be more productive as developers. And although you could implement this in the 25th hour of development, it won't hurt to start earlier. w::d


Luther Miller is a software architect/engineer at Softagon Corp. in San Francisco, where he has recently deployed the second phase of an order management and trading application for a hedge fund. He is an MCSD for .NET and focuses on solutions with .NET and SQL Server. Questions/comments are welcome at [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.