Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
778 C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S
Figure 23-1. Trying to request a forbidden web page
Authorization Rules
You define the authorization rules in the <authorization> element of the web.config file. The basic structure is as follows:
<authorization>
<allow users="comma-separated list of users" roles="comma-separated list of roles" verbs="comma-separated list of verbs" />
<deny users="comma-separated list of users" roles="comma-separated list of roles" verbs="comma-separated list of verbs" />
</authorization>
In other words, two types of rules exist: allow and deny. You can add as many allow and deny rules as you want. Each rule identifies one or more users or roles (groups of users). In addition, you can use the verbs attribute to create a rule that applies only to specific types of HTTP requests (GET, POST, HEAD, or DEBUG).
You’ve already seen the simplest example in the previous chapters. To deny access to all anonymous users, you can use a <deny> rule like this:
<authorization> <deny users="?" />
</authorization>
In this case, the question mark (?) is a wildcard that represents all users with unknown identities. This rule is almost always used in authentication scenarios. That’s because you can’t specifically deny other, known users unless you first force all users to authenticate themselves.
You can use an additional wildcard—the asterisk (*), which represents all users. For example, the following <authorization> section allows access by authenticated and anonymous users:
C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S |
779 |
<authorization> <allow users="*" />
</authorization>
This rule is rarely required, because it’s already present in the machine.config file. After ASP.NET applies all the rules in the web.config file, it applies rules from the machine.config file. As a result, any user who is explicitly denied access automatically gains access.
Now consider what happens if you add more than one rule in the authorization section:
<authorization>
<allow users="*" /> <deny users="?" />
</authorization>
When evaluating rules, ASP.NET scans through the list from top to bottom. As soon as it finds an applicable rule, it stops its search. Thus, in the previous case, it will determine that the rule <allow users="*"> applies to the current request and will not evaluate the second line. That means these rules will allow all users, including anonymous users. Reversing the order of these two lines, however, will deny anonymous users (by matching the first rule) and allow all other users (by matching the second rule).
<authorization>
<deny users="?" /> <allow users="*" />
</authorization>
When you add authorization rules to the web.config file in the root directory of the web application, the rules automatically apply to all the web resources that are part of the application. If you’ve denied anonymous users, ASP.NET will examine the authentication mode. If you’ve selected forms authentication, ASP.NET will direct the user to the login page. If you’re using Windows authentication, IIS will request user credentials from the client browser, and a login dialog box may appear (depending on the protocols you’ve enabled).
In the following sections, you’ll learn how to fine-tune authorization rules to give them a more carefully defined scope.
Controlling Access for Specific Users
The <allow> and <deny> rules don’t need to use the asterisk or question mark wildcards. Instead, they can specifically identify a user name or a list of comma-separated user names. For example, the following authorization rule specifically restricts access from three users. These users will not be able to access the pages in this directory. All other authenticated users will be allowed.
<authorization> <deny users="?" />
<deny users="dan" /> <deny users="jenny" /> <deny users="matthew" /> <allow users="*" />
</authorization>
You can also use a comma-separated list to deny multiple users at once. Here’s an equivalent version of the previous example that uses only two authorization rules:
<authorization> <deny users="?" />
<deny users="dan,jenny,matthew" /> <allow users="*" />
</authorization>
780 C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S
Note that in both these cases the order in which the three users are listed is unimportant. However, it is important that these users are denied before you include the <allow> rule. For example, the following authorization rules won’t affect the user jenny, because ASP.NET matches the rule that allows all users and doesn’t read any further:
<authorization> <deny users="?" />
<deny users="dan,matthew" /> <allow users="*" />
<deny users="jenny" /> </authorization>
When creating secure applications, it’s often a better approach to explicitly allow specific users or groups and then deny all others (rather than denying specific users, as in the examples so far). Here’s an example of authorization rules that explicitly allow two users. All other user requests will be denied access, even if they are authenticated.
<authorization> <deny users="?" />
<allow users="dan,matthew" /> <deny users="*" />
</authorization>
You should consider one other detail. The format of user names in these examples assumes forms authentication. In forms authentication, you assign a user name when you call the RedirectFromLoginPage() method. At this point, the UrlAuthorizationModule will use that name and check it against the list of authorization rules. Windows authentication is a little different, because names are entered in the format DomainName\UserName or ComputerName\UserName. You need to use the same format when listing users in the authorization rules. For example, if you have the user accounts dan and matthew on a computer named FARIAMAT, you can use these authorization rules:
<authorization> <deny users="?" />
<allow users="FARIAMAT\dan,FARIAMAT\matthew" /> <deny users="*" />
</authorization>
■Note Make sure you specify the computer or domain name in the users attribute when you use Windows authentication. You can’t use an alias such as localhost, because this will not be successfully matched.
Controlling Access to Specific Directories
A common application design is to place files that require authentication into a separate directory. With ASP.NET configuration files, this approach is easy. Just leave the <authorization> element in the normal parent directory empty, and add a web.config file that specifies stricter settings in the secured directory.
Remember that when you add the web.config file in the subdirectory, it shouldn’t contain any of the application-specific settings. In fact, it should contain only the authorization information, as shown here:
<configuration>
<system.web>
<authorization> <deny users="?" />
C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S |
781 |
</authorization>
</system.web>
</configuration>
■Note You cannot change the <authentication> tag settings in the web.config file of a subdirectory in your application. Instead, all the directories in the application must use the same authentication system. However, each directory can have its own authorization rules.
When using authorization rules in a subdirectory, ASP.NET still reads the authorization rules from the parent directory. The difference is that it applies the rules in the subdirectory first. This is important, because ASP.NET stops as soon as it matches an authorization rule. For example, consider an example in which the root virtual directory contains this rule:
<allow users="dan" />
and a subdirectory contains this rule:
<deny users="dan" />
In this case, the user dan will be able to access any resource in the root directory but no resources in the subdirectory. If you reverse these two rules, dan will be able to access resources in the subdirectory but not the root directory.
To make life more interesting, ASP.NET allows an unlimited hierarchy of subdirectories and authorization rules. For example, it’s quite possible to have a virtual directory with authorization rules, a subdirectory that defines additional rules, and then a subdirectory inside that subdirectory that applies even more rules. The easiest way to understand the authorization process in this case is to imagine all the rules as a single list, starting with the directory where the requested page is located. If all those rules are processed without a match, ASP.NET then begins reading the authorization rules in the parent directory, and then its parent directory, and so on, until it finds a match. If no authorization rules match, ASP.NET will ultimately match the <allow users="*"> rule in the machine.config file.
Controlling Access to Specific Files
Generally, setting file access permissions by directory is the cleanest and easiest approach. However, you also have the option of restricting specific files by adding <location> tags to your web.config file.
The location tags sit outside the main <system.web> tag and are nested directly in the base <configuration> tag, as shown here:
<configuration>
<system.web>
<!-- Other settings omitted. --> <authorization>
<allow users="*" /> </authorization>
</system.web>
<location path="SecuredPage.aspx"> <system.web>
<authorization>
<deny users="?" /> </authorization>
782 C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S
</system.web>
</location>
<location path="AnotherSecuredPage.aspx"> <system.web>
<authorization>
<deny users="?" /> </authorization>
</system.web>
</location>
</configuration>
In this example, all files in the application are allowed, except SecuredPage.aspx and AnotherSecuredPage.aspx, which have an access rule that denies anonymous users.
Controlling Access for Specific Roles
To make website security easier to understand and maintain, users are often grouped into categories, called roles. If you need to manage an enterprise application that supports thousands of users, you can understand the value of roles. If you needed to define permissions for each individual user, it would be tiring, difficult to change, and nearly impossible to complete without error.
In Windows authentication, roles are automatically available and naturally integrated. In this case, roles are actually Windows groups. You might use built-in groups (such as Administrator, Guest, PowerUser, and so on), or you can create your own to represent application-specific categories (such as Manager, Contracter, Supervisor, and so on). Roles aren’t provided intrinsically in forms authentication alone, but, together with Membership, ASP.NET employs the Roles Service, which is an out-of-the-box implementation for supporting and managing roles in your application. Furthermore, if you don’t want to use this infrastructure, it’s fairly easy to create your own system that slots users into appropriate groups based on their credentials. You’ll learn details about the two ways of supporting roles in the section “Using the Roles Service for Role-Based Authorization” in this chapter.
Once you have defined roles, you can create authorization rules that act on these roles. In fact, these rules look essentially the same as the user-specific rules you’ve seen already.
For example, the following authorization rules deny all anonymous users, allow two specific users (dan and matthew), and allow two specific groups (Manager and Supervisor). All other users are denied.
<authorization> <deny users="?" />
<allow users="FARIAMAT\dan,FARIAMAT\matthew" /> <allow roles="FARIAMAT\Manager,FARIAMAT\Supervisor" /> <deny users="*" />
</authorization>
Using role-based authorization rules is simple conceptually, but it can become tricky in practice. The issue is that when you use roles, your authorization rules can overlap. For example, consider what happens if you allow a group that contains a specific user and then explicitly deny that user. Or consider the reverse—allowing a user by name but denying the group to which the user belongs. In these scenarios, you might expect the more fine-grained rule (the rule affecting the user) to take precedence over the more general rule (the rule affecting the group). Or, you might expect the more restrictive rules to always take precedence, as in the Windows operating system. However, neither of these approaches is used in ASP.NET. Instead, ASP.NET simply uses the first matching rule. As a result, rule ordering can become important.
C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S |
783 |
Consider this example:
<authorization> <deny users="?" />
<allow users="FARIAMAT\matthew" /> <deny roles="FARIAMAT\Guest" /> <allow roles="FARIAMAT\Manager" /> <deny users="FARIAMAT\dan" />
<allow roles="FARIAMAT\Supervisor" /> <deny users="*" />
</authorization>
Here’s how ASP.NET parses these rules:
•In this example, the user matthew is allowed, regardless of the group to which he belongs.
•All users in the Guest role are then denied. If matthew is in the Guest role, matthew is still allowed because the user-specific rule is matched first.
•Next, all users in the Manager group are allowed. The only exception is users who are in both the Manager and Guest groups. The Guest rule occurs earlier in the list, so those users would have already been denied.
•Next, the user dan is denied access. But if dan belongs to the allowed Manager group, dan will already have been allowed, because this rule won’t be executed.
•Any users who are in the Supervisor group, and who haven’t been explicitly allowed or denied by one of the preceding rules, are allowed.
•Finally, all other users are denied.
Keep in mind that these overlapping rules can also span multiple directories. For example, a subdirectory might deny a user, while a parent directory allows a user in that group. In this example, when accessing files in the subdirectory, the user-specific rule is matched first.
File Authorization
URL authorization is one of the cornerstones of ASP.NET authorization. However, ASP.NET also uses another type of authorization that’s often not recognized. This is file-based authorization, and it’s implemented by the FileAuthorizationModule. File-based authorization takes effect only if you’re using Windows authentication. If you’re using custom authentication or forms authentication, it’s not used.
To understand file authorization, you need to understand how the Windows operating system enforces file system security. If your file system uses the NTFS format, you can set ACLs that specifically identify users and roles that are allowed or denied access to individual files. The FileAuthorizationModule simply checks the Windows permissions for the file you’re requesting. For example, if you request a web page, the FileAuthorizationModule checks that the currently authenticated IIS user has the permissions required to access the underlying .aspx file. If the user doesn’t, the page code is not executed, and the user receives an “access denied” message.
New ASP.NET users often wonder why file authorization needs to be implemented by a separate module—shouldn’t it take place automatically at the hands of the operating system? To understand why the FileAuthorizationModule is required, you need to remember how ASP.NET executes code.
Unless you’ve enabled impersonation, ASP.NET executes under a fixed user account, such as ASPNET. The Windows operating system will check that the ASPNET account has the permissions it needs to access the .aspx file, but it wouldn’t perform the same check for a user authenticated by IIS. The FileAuthorizationModule fills the gap. It performs authorization checks using the security
784C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S
context of the current user. As a result, the system administrator can set permissions to files or folders and control access to portions of an ASP.NET application. Generally, it’s clearer and more straightforward to use authorization rules in the web.config file. However, if you want to take advantage of existing Windows permissions in a local network or an intranet scenario, you can.
Authorization Checks in Code
With URL authorization and file authorization, you can control access only to individual web pages. The next step in ensuring a secure application is to build checks into your application before attempting specific tasks or allowing certain operations. To use these techniques, you’ll need to write some code.
Using the IsInRole() Method
As you saw in earlier chapters, all IPrincipal objects provide an IsInRole() method, which lets you evaluate whether a user is a member of a group. This method accepts the role name as a string name and returns true if the user is a member of that role.
For example, here’s how you can check if the current user is a member of the Supervisors role:
if (User.IsInRole("Supervisors"))
{
//Do nothing, the page should be accessed as normal because the
//user has administrator privileges.
}
else
{
// Don't allow this page. Instead, redirect to the home page. Response.Redirect("default.aspx");
}
Remember that when using Windows authentication, you need to use the format DomainName\ GroupName or ComputerName\GroupName. Here’s an example:
if (User.IsInRole(@"FARIAMAT\Supervisors")) { ... }
This approach works for custom groups you’ve created but not for built-in groups that are defined by the operating system. If you want to check whether a user is a member of one of the built-in groups, you use this syntax:
if (User.IsInRole(@"BUILTIN\Administrators")) { ... }
Of course, you can also cast the User object to a WindowsPrincipal and use the overloaded version of IsInRole() that accepts the WindowsBuiltInRole enumeration, as described in Chapter 22.
■Note The @ prefix when using strings in C# just enables you to use the backslash without escaping it with an additional backslash. This is especially useful if you have strings with lots of backslashes. But this also means you cannot use any escape sequence (such as \n or \r) in the string. If you want to use these escape sequences, you may not use the @ prefix. However, in this case, you have to escape any backslash; otherwise, the backslash would be used as the start of an escape sequence. This means with the @ prefix you would have to write FARIAMAT\\Supervisors, for example.
C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S |
785 |
Using the PrincipalPermission Class
.NET includes another way to enforce role and user rules. Instead of checking with the IsInRole() method, you can use the PrincipalPermission class from the System.Security.Permissions namespace.
The basic strategy is to create a PrincipalPermission object that represents the user or role information you require. Then, invoke the PrincipalPermission.Demand() method. If the current user doesn’t meet the requirements, a SecurityException will be thrown, which you can catch (or deal with using a custom error page).
The Demand() method takes two parameters—one for the user name and one for the role name. You can omit either one of these parameters by supplying a null reference in its place. For example, the following code tests whether the user is a Windows administrator:
try
{
PrincipalPermission pp = new PrincipalPermission(null, @"BUILTIN\Administrators");
pp.Demand();
//If the code reaches this point, the demand succeeded.
//The current user is an administrator.
}
catch (SecurityException err)
{
// The demand failed. The current user isn't an administrator.
}
The advantage of this approach is that you don’t need to write any conditional logic. Instead, you can simply demand all the permissions you need. This works particularly well if you need to verify that a user is a member of multiple groups. The disadvantage is that using exception handling to control the flow of your application is slower. Often, PrincipalPermission checks are used in addition to web.config rules as a failsafe. In other words, you can call Demand() to ensure that even if a web.config file has been inadvertently modified, users in the wrong groups won’t be allowed.
Merging PrincipalPermission Objects
The PrincipalPermission approach also gives you the ability to evaluate more complex authentication rules. For example, consider a situation where UserA and UserB, who belong to different groups, are both allowed to access certain functionality. If you use the IPrincipal object, you need to call IsInRole() twice. An alternate approach is to create multiple PrincipalPermission objects and merge them to get one PrincipalPermission object. Then you can call Demand() on just this object.
Here’s an example that combines two roles:
try
{
PrincipalPermission pp1 = new PrincipalPermission(null, @"BUILTIN\Administrators");
PrincipalPermission pp2 = new PrincipalPermission(null, @"BUILTIN\Guests");
// Combine these two permissions.
PrincipalPermission pp3 = (PrincipalPermission)pp1.Union(pp2); pp3.Demand();
//If the code reaches this point, the demand succeeded.
//The current user is in one of these roles.
786 C H A P T E R 2 3 ■ A U T H O R I Z AT I O N A N D R O L E S
}
catch (SecurityException err)
{
// The demand failed. The current user is in none of these roles.
}
This example checks that a user is a member of either one of the two Windows groups, Administrators or Guests. You can also ensure that a user is a member of both groups. In this case, use the PrincipalPermission.Intersect() method instead of PrincipalPermission.Union().
Using the PrincipalPermission Attribute
The PrincipalPermission attribute provides another way of validating the current user’s credentials. It serves the same purpose as the PrincipalPermission class, but it’s used declaratively. In other words, you attach it to a given class or method, and the CLR checks it automatically when the corresponding code runs. The exception handling now works a little bit differently: this time you cannot catch the exception within the function on which the attribute has been applied. You have to catch the exception in the function that actually calls this function. If you apply the PrincipalPermission attribute on an event procedure (such as Button_Click), you have to catch the exception in the global Application_Error event, which you can find in the global.asax file.
When you use a PrincipalPermission attribute, you can restrict access to a specific user or a specific role. Here’s an example that requires the user accessing the page to be in the server’s Administrators group. If the user is not member of the web server’s Administrators group, the ASP.NET runtime throws a security exception.
[PrincipalPermission(SecurityAction.Demand,
Role=@"BUILTIN\Administrators")] public class MyWebPage
{ ... }
Again, with the previous example you have to catch the exception in the global error handler (Application_Error) because your code is not the caller of this web page. Otherwise, ASP.NET would raise the exception and display the ASP.NET error page according to the web.config configuration. The following example restricts a particular method to a specific user:
[PrincipalPermission(SecurityAction.Demand, User=@"FARIAMAT\matthew")] private void DoSomething()
{ ... }
The caller of this method, of course, can catch the SecurityException with a try/catch block. PrincipalPermission attributes give you another way to safeguard your code. You won’t use
them to make decisions at runtime, but you might use them to ensure that even if web.config rules are modified or circumvented, a basic level of security remains.
■Note Changing declarative permissions means that you need to recompile the application. But why use them if every change requires recompilation? Don’t you want to have the possibility of managing roles in terms of adding, deleting, and changing them? Yes, and that requires more generic code, but it can’t be done with declarative permissions. So, when is it helpful to use declarative permissions? Well, declarative permissions are especially suited for fixed roles in your application that cannot be deleted anyway. For example, an Administrators role is required in most applications and therefore cannot be deleted. So, you can secure functionality that should be accessible to only administrators with declarative permissions. Typical examples in Windows are all the built-in groups such as Administrators, Power Users, Backup Operators, and Users.
