From the January 2002 issue of MSDN Magazine

MSDN Magazine

Managed Security Context in ASP.NET
Keith Brown
I

n my November 2001 column I focused on the unmanaged security context that is used in an ASP.NET application. It's very important to choose this security context wisely, as any calls to the operating system or to your own unmanaged DLLs and COM components will run here. This month, I'll focus on the way you can make use of your managed security context. ASP.NET provides several authentication mechanisms that result in a managed security context. Right now I'll stick with the Windows® authentication service for simplicity, but in a future column I'll discuss how you can use a variety of other authentication mechanisms, including forms and Microsoft® Passport.
      The Windows authentication provider relies on Internet Information Services (IIS) to authenticate requests based on metabase configuration settings. The ASP.NET ISAPI application runs in the Web server process and then simply hands the resulting token off to the ASP.NET worker process. Managed code in the worker process then creates a managed representation of this identity and assigns it to the thread it uses to call into your Web application. This managed representation is abstracted via two interfaces: IPrincipal and IIdentity.

IPrincipal and IIdentity

      Figure 1 shows these interfaces; as you can see, they are designed to be as minimal as possible. Note the complete lack of reliance on the Windows domain security model, which means all the techniques for doing role-based security that I describe here can be used with or without a dependency on native Windows authentication. You can roll your own authentication mechanism and still take advantage of the neat infrastructure that I'll discuss by providing implementations of these two interfaces.
      To be clear, these interfaces do not help you authenticate a user; rather, they represent the result of authentication. During steady-state operation of your application, these interfaces indicate whether the request is anonymous or has been authenticated. For authenticated requests, you can use IIdentity.Name in order to obtain a string representing the name of the principal, which will be in the form AUTHORITY\USER when you're using the Windows authentication provider. Other providers may use whatever format they want for representing user names.

Role-based Security

      From day one, Windows NT® was built with a role-based security architecture. The roles that the operating system itself relied upon were well-known local groups, with the canonical example being the Administrators local group. The operating system often looks explicitly for this group SID to allow or disallow certain privileged operations—for instance, allowing the installation or removal of services, device drivers, and so on. The benefit of relying on an abstract role (the Administrators local group, in this case) is that security policy can then be configured at a given site simply by assigning users to the Administrators group. Local groups were designed for use by applications in this way—each application could install its own set of local groups, and the administrator would map real users onto those groups in order to confer application-specific privileges.
      Microsoft Transaction Server (MTS) and later COM+ also provided a role-based infrastructure that basically did the same thing, allowing the developer to ship a set of abstract roles with his application. The administrators at the site installing the application were responsible for assigning users to those roles, essentially taking the set of abstract roles and making them concrete. The goal of all these architectures was to decouple design-time and install-time decisions. Roles have proven to be a good thing, and have therefore been carried forward via the IPrincipal interface, which allows managed applications to also depend on roles.
      When using the Windows authentication provider, roles are mapped directly onto Windows groups. For simplicity, in my examples I refer to a role called Supervisors, but in reality you'd need to use a fully qualified group name for the role if you rely on the Windows authentication provider.
      For instance, if you're using a domain-local group called MyDomain\Supervisors, you'd want to use that fully qualified group name in place of Supervisors wherever you see it in this column. If you use other authentication providers, such as forms-based authentication, these roles can be formatted however you want them to be, since you will be creating them yourself.

Imperative Role Checks

      Just like in MTS and COM+, it's easy to write code to check for the presence of a role:

  void StartTheMachinery() {
  
IPrincipal p = Thread.CurrentPrincipal;
if (!p.IsInRole("Supervisors")) {
string msg = "Only supervisors may " +
"start the machinery";
throw new SecurityException(msg);
}
// really start the machinery...
}

 

This is known as an imperative security check because you wrote code to perform the check explicitly.
      Imperative checks are often used in more complex scenarios. For instance, consider the classic example of a banking transaction, where a supervisor must perform withdrawals that exceed a certain amount of money (see Figure 2). In this case, the developer wove an imperative role check into the logic of his application.
      It turns out that each of these security checks could have been written a different way, using a PrincipalPermission object to perform the check. For instance:

  void StartTheMachinery() {
  
PrincipalPermission perm =
new PrincipalPermission(null, "Supervisors");
perm.Demand();

// if we're still here, really start the machinery...
}

 

This results in the same basic behavior as the earlier implementation. If the principal associated with the current thread is not in the Supervisors role, a SecurityException will be thrown before the sensitive code in the function gets a chance to run. In this case, the sensitive code starts up some machinery.

Declarative Role Checks

      While imperative role checks are very flexible, it often makes sense to embed this sort of authorization information into the design of the program itself through the use of metadata. Here's the same role check written declaratively:

  [PrincipalPermission(SecurityAction.Demand,
  
Role="Supervisors")]
void StartTheMachinery() {
// if we make it here,
// really start the machinery...
}

 

In this case, the Common Language Runtime (CLR) will demand that the principal associated with the calling thread is in the Supervisors role and will generate a SecurityException if the demand is not satisfied. Note that PrincipalPermissionAttribute is not a context attribute, so your object does not need to be context-bound to have this security check performed on its behalf. This functionality is baked into the runtime itself, so you don't need to worry about incurring the overhead of proxies and marshaling, and you aren't out of luck if your classes don't derive from ContextBoundObject. Figure 3 shows the intermediate language (IL) that results from compiling the previous code as a static method on a class; you should pay attention to the way the permission is XML-encoded into the assembly metadata.
      Here's an example where declarative checks really shine. You can protect an entire class, not just a single method of the class, simply by annotating the class with a PrincipalPermission attribute:

  [PrincipalPermission(SecurityAction.Demand,
  
Authenticated=true)]
public class Foo {
public void Bar() {
// caller MUST be authenticated
}
public static void Quux() {
// caller MUST be authenticated
}
}

 

      Bear in mind that you can use reflection to read these attributes from an assembly. This should make it easy to generate documentation for your classes, including which roles are allowed access to which classes and methods. This is another good reason to prefer using declarative checks over imperative checks wherever possible.
      Declarative role checks like this should feel familiar to COM+ programmers. The big difference is that the COM+ metadata was housed in a separate repository called the catalog. Under the CLR, metadata like this becomes a part of the component. In my opinion, keeping this sort of metadata close to the component is really important because COM+ components usually don't function correctly, or will be insecure, if the catalog settings for that component are changed by someone other than the component designer herself. By placing these attributes inside the assembly close to the component, these extra semantic requirements become easier to maintain later on and can even be protected via module and assembly hashes and signatures.
      Another reason I prefer CLR role-based security over COM+ role-based security is that in the CLR, security is decoupled from the Windows domain security model. This makes it flexible enough to use with your own authentication mechanism, which for Web applications is typically forms-based. COM+ roles only work with Windows security accounts. Depending on what you're building, you will often not want to add new native operating system accounts for Web clients.

Role Checks on Web Pages

      All the examples I've given so far can be used in Web applications; you can use these checks directly in the code for your Web page, or in classes used by your Web page. As I discussed in the November 2001 column, ASP.NET will set the thread principal to either an anonymous principal or, if authentication has occurred, to a principal representing the client. All you need to do is check this information in your page.
      For instance, with code-behind it's possible to use declarative security checks to control access to individual pages:

  <%@page language='c#'
  
inherits='Foo' src='foo.cs'%>
<h1>If you see this you must be a Supervisor</h1>

 

This page is not very interesting until you look at the associated code-behind class. Note the attribute I've used in the following code to protect the page against unauthorized use.

  [PrincipalPermission(SecurityAction.Demand,
  
Role="Supervisors")]
public class Foo : System.Web.UI.Page {
// ...
}

 

This prevents the page from running unless the request comes from someone in the Supervisors role. Since each page you design in ASP.NET is converted into a managed class the first time the page is accessed, when ASP.NET tries to instantiate this particular page class, the constructor for its base class (in this case Foo) fails unless the request is coming from a Supervisor, and the page factory receives a SecurityException.
      If you plan on using this approach in your own project, please be sure to read the rest of this column because you'll need to do a little extra work to make sure that authorized users aren't accidentally denied access to the pages because of this security exception.

Role Checks via Web.config

      You can provide role-based checks for entire sets of Web pages by simply putting the restricted pages in a subdirectory under the application root, and adding a Web.config file to that folder as well.

  <configuration>
  
<system.web>
<authorization>
<allow roles='Supervisors'/>
<deny users='*'/>
</authorization>
</system.web>
</configuration>

 

In this case, an HttpModule called UrlAuthorizationModule performs the role check and returns a 401 error to the Web server if the check fails, forcing the Web server to authenticate an anonymous client or simply to fail the request. If you've never heard of an HttpModule before, let me say that an HttpModule is to an ISAPI filter what an HttpHandler is to an ISAPI extension. HttpModules are managed classes that live in the ASP.NET worker process and get to process each request before the corresponding handler, as shown in Figure 4. So UrlAuthorizationModule sees all requests that will eventually hit any of your ASP.NET pages, and can halt an unauthorized request early, even before it gets to your page.

Figure 4 HttpHandler
Figure 4 HttpHandler

      Note the careful way I've constructed this authorization section. First I grant access to whomever I want to be able to access the pages in this directory, but then I explicitly deny access to all users. The way UrlAuthorizationModule works is by taking all applicable Web.config files, starting from the one closest to the path requested by the client, back up the directory structure all the way to the virtual root, and concatenates the <authorization> sections as it goes. The last config file to be added to this authorization list is machine.config, which by default looks like this:

  <authorization>
  
<allow users='*'/>
</authorization>

 

So in my case, the list would stack up like so:

  <authorization>
  
<allow roles='Supervisors'/>
<deny users='*'/>
<allow users='*'/>
</authorization>

 

      Once UrlAuthorizationModule concatenates all applicable authorization sections, it then walks the list from top to bottom looking for a match. As soon as a match is found, access is granted or denied immediately based on whether the matching entry is an <allow> or a <deny> element. So the reason I added the <deny> entry to my Web.config file was to override the default setting in machine.config that grants everyone access.
      You can also use Web.config to make blanket statements about how to handle unauthenticated (also known as anonymous) requests with a single tag:

  <authorization>
  
<deny users='?'/>
</authoriztion>

 

To summarize, the wildcards that UrlAuthorizationModule knows about are as follows:
      * = all requests
      ? = anonymous requests

Unhandled Security Exceptions

      If you're going to use the Windows security provider, you're relying on IIS to authenticate your users via one of its built-in authentication mechanisms: Basic, Digest, Integrated, and so on. Most Web sites that use one of these authentication mechanisms allow anonymous users as well. The key thing you need to understand is that whenever anonymous users are allowed, IIS does not normally bother to authenticate any user unless it absolutely must. That is, all users will be anonymous unless IIS receives an HTTP 401 status code (Unauthorized) from the code handling the request. The reason for this is that authentication places an extra load on the server, and often an extra burden on the user by forcing him to enter credentials manually. Since many Web pages don't require authentication, IIS doesn't authenticate unless someone indicates that it's really necessary.
      If you allow a SecurityException to propagate up to ASP.NET, the current incarnation of ASP.NET simply presents the user with a generic error message. My earlier example of placing a PrincipalPermission attribute on a code-behind class will cause this to happen, for instance. This is bad because the user will never have a chance to prove that he is indeed a Supervisor. Each time he reaches the page, he does so as an anonymous user by default, so you need to convert any SecurityException you throw into HTTP 401 status code to indicate to IIS that you want to authenticate the client.
      How can you provide a generic unhandled exception filter for your ASP pages? It's actually quite easy; one good solution is to write an HttpModule of your own that traps errors. Figure 5 shows a very simple example that looks for SecurityExceptions and converts them into 401 codes. Assuming you compile this class into an assembly, say asm.dll, and install it in the bin subdirectory of your virtual root, here's the code that you'll need to add to your Web.config file to add the assembly into the ASP.NET HTTP pipeline:

  <configuration>
  
<system.web>
<httpModules>
<add name='MyModule' type='MyModule, asm'/>
</httpModules>
</system.web>
</configuration>

 

      Note that when MyModule checks for exceptions, it doesn't simply assume that the SecurityException will present itself in the raw. For instance, if the page fails to be created because of the PrincipalPermission attribute on the code-behind class, you'll find that the SecurityException is caught by the code attempting to create the page class. A different exception is thrown in its place, but the ASP.NET team carefully embeds the original exception as the InnerException of the exception they throw. In short, don't just check the AllErrors collection; rather, for each exception you find there, drill down recursively looking for a SecurityException that was caught and rethrown. This is what the hasSecurityException function does in Figure 5. Another way to host this sort of error checking code would be to implement the Application_OnError event in global.asax.
      I should also mention that if you're going to use some other authentication mechanism, such as forms-based authentication, you can still use an HttpModule to trap unhandled SecurityExceptions from your pages. However, returning a 401 code to IIS is inappropriate if you don't want IIS to authenticate the client. In this case you should simply do whatever you'd normally do to authenticate the user—for instance, you can redirect the client to a forms-based login page instead.

Preventing Fraud

      In my previous column, I explained that the impersonation mechanism provided by Win32® was designed for the convenience of server developers and not as a way to stop untrusted code from doing bad things. For instance, a compromised server-side component can simply remove any low-privileged thread token by calling RevertToSelf, and if it's running in the Web server process, in all likelihood it will now be running as SYSTEM because the impersonation token will have been stripped off.
      Well, check out the code in Figure 6 and see if it doesn't look hauntingly familiar. Is this possible? Absolutely. The big difference is that unlike Win32, there is a very tight security infrastructure underneath your code that can prevent this sort of fraud. It's called code access security (CAS) and I strongly recommend that you use it to avoid this sort of funny business. By default, assemblies that come from the My Computer Zone (in other words, assemblies that are installed on a local device) are fully trusted. They can play games like this.
      You can use a CAS permission called SecurityPermission to disallow changes to any thread's principal, or the application domain's principal policy. I'd recommend that you consider adjusting your CAS security policy and close it down a bit on your Web server. Remember that it's possible to use URL membership conditions to partition your local hard drive so that assemblies in certain directories have more or fewer privileges than those in other directories. Web pages that rely on role-based security shouldn't be given the right to change the principal, especially if third-party components are involved.

A Parting Note

      Remember my plea in November for developers to stop running with admin privileges when developing code? I thought you might find it interesting that I recently taught my first class at DevelopMentor where all my students ran as normal users. The classrooms are normally set up such that each student is an administrator on their own machine, but we fixed that on day one. Everyone agreed to try running as a normal user, using the runas feature I described in the November column to temporarily elevate privileges to install and configure applications. The main annoyances people had were with Darwinian loops in Visual Studio® 7.0, which I'm hoping will be fixed before its release, and the fact that some development tools don't separate the build step from the installation step.
      For instance, when you build an ATL COM server using Visual Studio, the last step of the build is to register the server. This step will fail if you're not an administrator. What's the solution? If you need to register your components after a build, write a batch file that registers them and simply run that batch file from a command prompt running as Administrator. There's no reason to run the development environment itself with admin privileges. Most development shops end up with a custom build process (often using tools like make or nmake). Consider factoring these tools so that any user whatsoever can run the build step, and only the install step requires administration privileges.

Send questions and comments for Keith to briefs@microsoft.com.

Keith Brown works at DevelopMentor researching, writing, teaching, and promoting an awareness of security among programmers. Keith authored Programming Windows Security (Addison-Wesley, 2000). He coauthored Effective COM, and is currently working on a .NET security book. He can be reached at https://www.develop.com/kbrown.