Problem
We have a very simple ASP.NET web site that uses the built in Forms Authentication provider out of the box for users, roles and security. Our internal business customers have hundreds of PDF documents that should only be accessible for specific users or roles within the company. A majority of users that need access to these files are outside the company firewall and do not have VPN access. The site must be hosted on Windows Server 2003 using IIS 6.
Although this problem could be solved in many ways, I wanted to make the site as dynamic as possible since our web master would need to make constant changes to files, structure, etc and we did not want a programmer having to make changes to specific code or a DBA maintaining a database of files, paths, etc. We also wanted to avoid using a network file share to host the files or wrap the site using Plain Text authentication.
The issue is Forms Authentication provides no security on the request stack within IIS 6 as it does in IIS 7. Because we can't extend this, our files would be wide open to any attacker or malicious employee looking to download the file by figuring out the link or by users who had bookmarked certain files and later after they had been removed from that role, being able to still access them.
The Solution
The solution seemed easy. Some content management systems allow you to store files in databases, or use URL re-writing to accomplish this task, but I wanted something simpler and easier to extend or duplicate in the future. We're all pretty familiar with IHttpHandler and it seemed since that's the way the prior, more complex solutions perform this feat more often than not, why not give it a shot for this:
Setup
In order for ASP.NET to recognize the request for the PDF file and wrap it with the configured authentication method, we have to tell IIS to use the same ASAPI engine that is uses for ASPX, ASMX, etc.
I opened the properties for the web site/virtual directory in IIS Manager, clicked on the Configuration… button located within the application settings section and the first tab contains my ISAPI extensions:
Then, I clicked the "Add…" button to add the new .pdf extension using the same ASP.NET ISAPI DLL, c:\windows\microsoft.net\framework\v2.0.50727\aspnet_isapi.dll for GET requests of .pdf:
I'm using the "Verify that file exists" because the file really will exists, it's not in a database or remote location or anything fancy like that.
Once that's done, you'll notice if you have security set up on your site and in your web.config file around a PDF file, say for:
<location path="MyFile.pdf"><web.config><authorization><deny users="?" /></authorization></web.config></location>
And you are not authenticated; you will immediately be taken to the login screen. Once you log in however you will get a compilation error page. This is because the ASP.NET CLR is trying to compile/execute your PDF as an ASP.NET extension file and it cannot for obvious reasons, it doesn't know how to handle your file.
IHttpHandler
Now that security is taken care of, I needed to let ASP.NET know what to do with a PDF file. To do this I needed to create a simple IHttpHandler:
public class PdfHandler : IHttpHandler
{
public PdfHandler() { }
public void ProcessRequest(HttpContext context)
{
string path = context.Request.PhysicalPath;
string name = path.Split('\\')[path.Split('\\').Length - 1];
if (!string.IsNullOrEmpty(path) && path.ToLower().EndsWith(".pdf"))
{
context.Response.ClearHeaders();
context.Response.ClearContent();
context.Response.Clear();
context.Response.Charset = null;
context.Response.ContentType = "application/pdf";
context.Response.AddHeader("Content-Type", "application/pdf");
context.Response.AppendHeader("Content-Disposition", string.Format("inline;filename={0}", name));
context.Response.WriteFile(path);
}
else
throw new FileNotFoundException("The page requested is invalid", path);
}
public bool IsReusable { get { return false; } }
}
This class, PdfHandler, implements the IHttpHandler interface which implements a proper, IsReuseable (which this one is not) and a method, ProcessRequest, which does the dirty work using the passed in HttpContext. In this case, I get the physcial path of the file being requested (remember that the file should always exist, otherwise IIS would have kicked the request back out) and then parse the name of the file, double check (using some sanity checking) that the file is indeed a PDF that we're dealing with, clearing the response, setting the MIME type in the header and using the Response.WriteFile(filePath) method to output the file directly through the buffered response stream to the client. This would all be transparent to the user and the response time, although not as fast as IIS serving the file directly, is still acceptible for my means of use.
Web.Config Changes
Once I had the handler written for my PDF files, I just needed to let ASP.NET on the application level to use my handler for all PDF requests coming into the virtual directory/web site. I did this by using the configuration section under <web.config>, <httpHandlers>:
<httpHandlers>
<add verb="GET" path="*.pdf" type="PdfHandler" validate="false"/>
</httpHandlers>
With this entry in there now, everything is working great and now my web master and I can manage security in the application simply by adding folders and new web.config files with an <authorization/> section along with the built-in membership/role management that comes standard in ASP.NET application services.
Now if only I can get them on IIS 7, this entire article would become obsolete, but life would definitely be better!