In the previous two parts (Part 1 and Part 2), I introduced the ImpostorHttpModule as a way to test intranet applications that use role-based security without having to modify your group memberships. (I’ll assume that you know what I’m talking about. If not, go back and re-read the first two parts.) In the final part, let’s look at what exactly is going on behind the scenes with ImpostorHttpModule…
The ImpostorHttpModule requires surprisingly little code to work its magic. Let’s think about exactly what we want to do. We want to intercept every HTTP request and substitute the list of roles defined for the incoming user in the ~/App_Data/Impostors.xml file instead of the user’s actual roles. (In an intranet scenario, a user’s roles are often just the local and domain groups to which the user belongs.) To do this, we need to implement a HttpModule. We’ll start with the simplest HttpModule, which we’ll call NopHttpModule for “No operation”.
using System.Web;
namespace JamesKovacs.Web.HttpModules {
public class NopHttpModule : IHttpModule {
public void Init(HttpApplication context) {
}
public void Dispose() {
}
}
}
To be a HttpModule, we simply need to implement IHttpModule and provide implementations for the two methods, Init() and Dispose(). We now have to register ourselves with the ASP.NET pipeline. We do this using the <httpModules> section of Web.config.
<?xml version=”1.0″?>
<configuration>
<system.web>
<httpModules>
<add name=”NopHttpModule” type=”JamesKovacs.Web.HttpModules.NopHttpModule, JamesKovacs.Web.HttpModules”/>
</httpModules>
</system.web>
</configuration>
That’s it. Not terribly interesting because it does absolutely nothing. So let’s move on and implement the HelloWorldHttpModule, which simply returns “Hello, world!” no matter what you browse to, whether it exists or not!
using System;
using System.Web;
namespace JamesKovacs.Web.HttpModules {
public class HelloWorldHttpModule : IHttpModule {
public void Init(HttpApplication context) {
context.BeginRequest += new EventHandler(context_BeginRequest);
}
void context_BeginRequest(object sender, EventArgs e) {
HttpContext.Current.Response.Write(“<html><body><h1>Hello, World!</h1></body></html>”);
HttpContext.Current.Response.End();
}
public void Dispose() {
}
}
}
Try browsing to /Default.aspx, /Reports/Default.aspx, /ThisDoesNotExist.aspx, or even /ThisDoesNotExistEither.jpg. They all return “Hello, World!” (N.B. ASP.NET 1.X will return a 404 for the JPEG. ASP.NET 2.0 will return “Hello, World!” In 1.X, static
files were served up directly by IIS without ASP.NET getting involved. Although this gives excellent
performance for images, CSS, JavaScript files, etc., it also meant that
those files were not protected by ASP.NET security. With ASP.NET 2.0, all unknown files types are handled by
the System.Web.DefaultHttpHandler, which allows non-ASP.NET resources to be protected by ASP.NET security as well. See here for more information.)
Now back to our regularly scheduled explanation… In our Init() method, we tell the HttpApplication which events we would like to be informed of. In this case, we grab the BeginRequest event, which is the first event of the ASP.NET pipeline. It occurs even before we determine if the URL is valid, hence our ability to serve up “missing content”.
ASP.NET provides many hooks into its processing pipeline. Here is an excerpt from MSDN2 on the sequence of events that HttpApplication fires during processing:
- BeginRequest
- AuthenticateRequest
- PostAuthenticateRequest
- AuthorizeRequest
- PostAuthorizeRequest
- ResolveRequestCache
- PostResolveRequestCache
After the PostResolveRequestCache event and before the PostMapRequestHandler event, an IHttpHandler (a page or other handler corresponding to the request URL) is created.
- PostMapRequestHandler
- AcquireRequestState
- PostAcquireRequestState
- PreRequestHandlerExecute
The IHttpHandler is executed.
- PostRequestHandlerExecute
- ReleaseRequestState
- PostReleaseRequestState
After the PostReleaseRequestState event, response filters, if any, filter the output.
- UpdateRequestCache
- PostUpdateRequestCache
- EndRequest
The pipeline in ASP.NET 1.X had many, but not all, of these events. ASP.NET 2.0 definitely gives you much more flexibility in plugging into the execution pipeline. I’ll leave it as an exercise to the reader to investigate why you might want to capture each of the events.
Armed with this information, you can probably figure out which event we want to hook in the ImpostorHttpModule. Let’s walk through the thought process anyway… We are trying to substitute the actual user’s roles/groups for one that we’ve defined in the ~/App_Data/Impostors.xml file. To do this we need to know the user. So we need to execute after the user has been authenticated. We need to execute (and substitute the groups/roles) before any authorization decisions are made otherwise you might get inconsistent behaviour. For instance, authorization may take place against your real groups/roles and succeed, but then a PrincipalPermission demand for the same group/roles might fail because the new groups/roles have been substituted. So which event fits the bill? PostAuthenticateRequest is the one we’re after. In this event, we know the user, which was determined in AuthenticateRequest, but authorization has not been performed yet as it occurs in AuthorizeRequest.
public void Init(HttpApplication context) {
context.PostAuthenticateRequest += new EventHandler(context_PostAuthenticateRequest);
}
We know which event we want to hook. Now what to do once we hook it. In .NET, we have Identities and Principals. An Identity object specifies who has been authenticated, but does not indicate membership in groups/roles. A Principal object encapsulates the groups/roles and the identity. So what we want to do is construct a new Principal based on the authenticated Identity and populate with the groups/roles that we read in from ~/App_Data/Impostors.xml. As it so happens, the built-in GenericPrinicpal fits the bill quite nicely. It takes a IIdentity object and a list of roles (in the form of an array of strings). N.B. It doesn’t matter if the Identity is a WindowsIdentity, a FormsIdentity, a GenericIdentity, or any other. All that matters is that the Identity implements the IIdentity interface. This makes the group/role substitution code work equally well regardless of authentication technology.
IIdentity identity = HttpContext.Current.User.Identity;
string[] roles = lookUpRoleListFromXmlFile(identity); // pseudo-code
IPrincipal userWithRoles = new GenericPrincipal(identity, roles);
Armed with userWithRoles, we just need to patch it into the appropriate places:
HttpContext.Current.User = userWithRoles;
Thread.CurrentPrincipal = userWithRoles;
We have discarded the original principal (but kept the original identity) and patched in our custom one. That’s about it. Any authorization requests are evaluated against the new GenericPrincipal and hence the group/role list that we substituted.
An additional feature I would like to point out is caching of the users/roles as you probably don’t want to parse a XML file on every request. The users/roles list will auto-refresh if the underlying ~/App_Data/Impostors.xml file changes. Let’s see how this works. We store a Dictionary<string, string[]> in the ASP.NET Cache, which contains users versus roles as parsed from the ~/App_Data/Impostors.xml file. If it doesn’t exist in the Cache, we parse the XML file and insert it into the Cache along with a CacheDependency like this:
HttpContext.Current.Cache.Insert(“ImpostorCache”, impostors, new CacheDependency(pathToImpostorsFile));
When the underlying file changes, the entry is flushed from the cache. The next time the code runs, the cache is re-populated with the contents of the updated ~/App_Data/Impostors.xml.
One last point… The ImpostorHttpModule is meant for development/testing purposes, which means that I haven’t optimized it for performance, but for ease of implementation and comprehension.
So there you have it – the ImpostorHttpModule. Hopefully you have a better appreciation for the power and extensibility built into ASP.NET as well as some cool ideas of what else you can implement using HttpModules. Full source code can be found here.