Last week, I ran into an interesting problem that I didn't immediately find a solution for. I was cleaning up one of my DevConnections samples, which is a photo album application. The idea is that any folder that contains images, multi-media content or anything else can be turned into a dynamic photoalbum by copying a couple of files into this folder that can then be accessed from the Web. What I needed to do is have a dynamic URL that when executed in this directory automatically creates the photo album related files in the current folder.

These administrative functions are blocked off via Authentication - either Forms or Windows Authentication - so that the admin features are not accessible to all.  So I require that the user is logged in - otherwise a 401 status is thrown which SHOULD in theory redirect the user to the login page in the case of Forms Auth or the Windows Logon with Windows Auth.

So for example, if I have the application set up in localhost/photoalbum and I add a new folder underneath it called Maui2007 I should be able to create a new album like this:

http://localhost/photoalbum/maui2007/CreateAlbum.axd

This is what would amount to a virtual URL - a URL that has no backing file on disk. ASP.NET makes it pretty easy to create virtual URLs. In IIS 7's integrated pipeline  ALL files are routed through the managed pipeline and an HttpModule (or Application_ handler) can look at all files with any extension, from ASPX pages to images and html files inside of a module. In IIS 5/6 you can only look at URLs that are routed to the ASP.NET ISAPI extension which requires some configuration.

I chose AXD as the extension because AXD is a built in and already a virtual extension. By that I mean it's is mapped in IIS as not requiring a mapping file. AXD is used extensively by ASP.NET internally - WebResource.axd, Trace.axd, ScriptResource.axd are a few of the more common ones you might have seen before.

So I created a Module that looks something like this:

public class PhotoAlbumModule : IHttpModule
 { 
    public void Dispose()
    {
    }
 
    public void Init(HttpApplication context)
    {
        context.PostAuthorizeRequest += new EventHandler(context_PostAuthorizeRequest);
    }
 
    void context_PostAuthorizeRequest(object sender, EventArgs e)
    {
        HttpContext Context = HttpContext.Current;
        HttpRequest Request = Context.Request;
 
        string script = Path.GetFileName(Request.FilePath).ToLower();                            
        if (script != "createalbum.axd" && script != "updatetemplate.axd")
            return;
 
        // *** Must be authenticated
        if (Context.User == null || !Context.User.IsAuthenticated)
        {
            Context.Response.StatusCode = 401;
            Context.Response.End();
            return;
        }
 
        if (script == "createalbum.axd")
        {
            this.CreateAlbum();
        }
        else if (script == "updatetemplate.axd")
        {
            this.UpdateTemplate();
        }
    }
 
    void CreateAlbum()
    {
        HttpContext Context = HttpContext.Current;
        HttpRequest Request = Context.Request;
 
        string physicalPath = Path.GetDirectoryName(Request.PhysicalPath);            
 
        File.Copy(Context.Server.MapPath("~/Templates/showphotos.aspx"),
                   Path.Combine(physicalPath,"default.aspx"),true );
 
        string newUrl = Path.Combine(Path.GetDirectoryName(Request.FilePath), "default.aspx");
 
        Context.Response.Redirect(newUrl);
        Context.Response.End();
    }
 
    void UpdateTemplate()
    {
        this.CreateAlbum();
    }
 }

Where's my Authentication on .AXD files?

But I quickly ran into a problem with this code when running under IIS 7.  When running in IIS 7 and Integrated security I found that although the user was authenticated and other URLs show an Context.User, .AXD files do not actually have the authentication applied to them.

This code:

        // *** Must be authenticated
        if (Context.User == null || string.IsNullOrEmpty(Context.User.Identity.Name))

always fails with Context.User == null on an .AXD file but works fine on ASPX or ASHX files when running with Forms Auth.

Even more interesting when the code would fire the Status = 401 which normally fires off to the Login page, IIS 7 would simply throw up a 401 error page. In other words it looks just like IIS is completely bypassing Forms Authentication on the .AXD extension URL. So hitting the CreatePhotoAlbum.axd results in an IIS 7 error page rather than a login prompt either for Windows Auth or for Forms Auth.

Running the same code under IIS 6 (or rather on IIS 7 in ASP.NET 2.0 compatibility mode) the code works fine with all files that are mapped through ASP.NET (via ScriptMapping extension into the ASP.NET ISAPI extension), including the .AXD files being properly authenticated when using Forms Authentication. Upon closer examination it appears that only certain extensions actually get authenticated in IIS 7. ASPX and ASHX work, but  image files, css, js files etc. also don't show up with authentication in PostAuthorizeRequest even though they do fire through my module. 

For kicks I experimented around with a different pipeline events like PreRequestHandlerExecute() which is even later in the ASP.NET pipeline to see if that would change anything, but no luck. I'm not sure what rules are applied what goes through forms auth and what doesn't but apparently not everything and not even all ASP.NET mapped extensions (ie. .AXD).

Another Solution

In the end the solution for my particular problem was to use .ASPX as the extension (ie. CreateAlbum.aspx), which seems to work and get me the authenticated user both on IIS 5/6 and 7.  Originally I didn't want to use ASPX as the extension because I thought (incorrectly) that the default mappings in IIS require a mapping file to be present, but a quick check confirms that this is not the case - the check for 'Check if File/Directory exists' is not checked in IIS 7 or IIS 5/6 so using ASPX for 'virtual urls'  works fine even if it's slightly unconventional (people will go looking for a file that ain't there <g>).

It took the above failures (and a couple of pokes from Twitter) to realize that ASPX would work fine, but that's how it goes sometimes...

But this is a somewhat important issue to keep in mind. Authentication of certain files works differently in IIS 7 than it does in previous versions of IIS. I tried looking around in IIS 7's applicationhost.config file to see if there's any mapping of extensions to authentication flags, but couldn't find anything specifically. <shrug> Maybe somebody from the IIS team has a little more information

But this is something to keep in mind if you want to apply system level security to none Page resources in your application. Apparently authentication is not applied against them and Context.User is either null (Forms Auth) or the User details are blank (!Context.User.Identity.IsAuthenticated)  (Windows Auth).