Securing PDF Files in ASP .NET with Custom HTTP Handlers

Introduction

It’s quite common to include PDF and other static file formats in a C# ASP .NET web application for access by the user. However, many developers overlook the fact that the built-in .NET authentication and security framework does not apply to static files, such as PDF files, Word docs, Excel reports, and other documents included within the web application. While the web application may have been designed to only allow authenticated users to access the web site after logging in, it’s possible (depending on the web.config) that any user could access the static documents, provided they know the direct URL pointing to the file. In addition, the web application would be unable to restrict logged-in user access by the User.Identity account. An obvious solution might be to use the IIS file security via Integrated Windows Authentication, requiring an awkward Windows username and password prompt upon accessing the URL. However, this method would still not allow functionality for multiple types of users, each with their own access level within the web application. In this case, certain users can access certain documents and the typical IIS security would not suffice. This is where a custom HTTP handler provides a solution.

In this article, we’ll create a basic C# ASP .NET web application which allows two types of users to login to the system. The web application will use the .NET built-in authentication manager and role provider. With a simple security framework setup, we’ll place a PDF file in the web application, behind a web.config protected folder. We’ll then create a custom HTTP handler to restrict access on the static document to only those users who should be allowed to view it.

The Problem with IIS Security

The first thought which comes to mind when securing a static file within a web application is to allow IIS to control the security by password protecting the file with a Windows account, such as with Integrated Windows Authentication, etc. In this scenario, a Windows username/password prompt will display when a user tries to access the URL pointing to the file. The first problem with this approach is the Windows dialog itself. The dialog sends a clear message to the user that it is not part of the web application. In fact, the username and password itself may not even be part of the web application’s authorization list. There are also security concerns to address, such as the username/password provided to your users and the management of these accounts. However, we can do better with ASP .NET.

A Glance at the Custom HTTP Handler

Before we dive into the details of the C# ASP .NET custom HTTP handler, let’s take a quick look at how it works to solve our issue of protecting and securing the static PDF file in our web application.

A Custom HTTP Handler is a class which sits between IIS and the main web application’s processing module. It allows you to pre-process HTTP GET and POST requests before the main web application pages begin processing. With this ability, you can intercept requests for static files, such as PDF or Word documents, and apply your web application’s security for the currently logged-in user, before allowing access.

Since Visual Studio’s built-in web server, Cassini, automatically sends requests for all document file types to the web application, we can easily test our custom HTTP handler. However, when installing the application on IIS, we’ll need to configure an application extension mapping (or, in IIS 7, a managed handler). For now, let’s move on to setting up the test application framework to allow users to login to the web application.

Getting Started with Login Basics

Download the project source code.

The test web application will contain a Login.aspx page and a /User directory, which only authenticated users may access. The /User directory will contain a /Documents folder, containing the PDF file, which we need protected. The PDF file will only be accessible to users containing the role “User”. All other users logged into the system will be denied access to the file.

1
2
3
4
5
6
Root Web Site
Login.aspx
-- User
Default.aspx
-- Documents
PDF1.pdf

To begin, we’ll need to create a very basic login page. We can do this by simply adding an ASP .NET Login control to the page. Set the DestinationPageUrl property to ~/User/, since we want all users directed to this page upon login. The login control will need a Membership Provider and Role Provider in order to populate the User.Identity object correctly. We’ll be using the User object in our web application to determine access permissions to our PDF file.

A Simple Membership Provider

To create a basic custom membership provider, we’ll simply create a class called MyMembershipProvider, which inherits from the System.Web.Security base class MembershipProvider. We’ll implement the single method ValidateUser(), which is the minimum to utilize this class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
if (username == "john" && password == "doe")
{
return true;
}
else if (username == "jane" && password == "doe")
{
return true;
}
else
{
return false;
}
}

...
}

Note in the above code, we’ve defined a very basic authentication routine for allowing the users john and jane to login to the web application. Obviously, you would replace this with actual authentication via a database, Active Directory, or other means. All other methods inherited from the base class are left as undefined.

A Simple Role Provider

We’ll also need a very basic role provider to add the two defined roles for our logged-in users. One role, “User”, will be allowed access to the PDF file. The other role, “Guest”, will not be allowed access to the PDF file, although they can both login to the web application. These two roles help demonstrate how you can allow or deny access to static files from within the ASP .NET web application.

We’ll create a class called MyRoleProvider, which inherits from the System.Web.Security base class RoleProvider. We’ll only implement a few of the base class methods, as needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyRoleProvider : RoleProvider
{
...

public override string[] GetAllRoles()
{
return new string[] { "User", "Guest" };
}

public override string[] GetRolesForUser(string username)
{
if (username == "john")
{
return new string[] { "User" };
}
else if (username == "jane")
{
return new string[] { "Guest" };
}
else
{
return new string[] { };
}
}

public override bool IsUserInRole(string username, string roleName)
{
string[] roles = GetRolesForUser(username);
return (roles.ToList().IndexOf(roleName) > -1);
}

public override bool RoleExists(string roleName)
{
string[] roles = GetAllRoles();
return (roles.ToList().IndexOf(roleName) > -1);
}
}

Notice that we’ve defined two roles, User and Guest. We assign the roles according to the logging-in username. Of course, in your C# ASP .NET web application, these roles would probably be read from a database per username. For simplicity, they’ve been added in the sample web application as hard-coded plain text.

For more details about creating custom membership and role providers in ASP .NET, see this article.

Plugging in the Web.Config

The final piece for our web application authentication framework is to set the required configuration settings in the web.config to allow users to login. We’ll start by enabling forms authentication, as follows:

1
2
3
<authentication mode="Forms">
<forms loginUrl="Login.aspx" name="MY_AUTH" slidingExpiration="true" timeout="20"/>
</authentication>

Next, we can add settings for our membership and role provider, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear/>
<add name="MyMembershipProvider" type="CustomFileHandlerDemo.Providers.MyMembershipProvider, CustomFileHandlerDemo"/>
</providers>
</membership>

<roleManager defaultProvider="MyRoleProvider" enabled="true" cacheRolesInCookie="true" cookieName="MyRoles" cookieTimeout="30" cookiePath="/" cookieRequireSSL="false" cookieSlidingExpiration="true" cookieProtection="All">
<providers>
<clear/>
<add name="MyRoleProvider" type="CustomFileHandlerDemo.Providers.MyRoleProvider, CustomFileHandlerDemo" applicationName="CustomFileHandlerDemo" writeExceptionsToEventLog="false"/>
</providers>
</roleManager>

These definitions allow the ASP .NET Login control to use our custom membership and role providers to setup the User.Identity object.

We have one final web.config setting to make in order to pre-process requests to PDF files in our web application. However, let’s first create our file protection handler.

Where All The Magic Happens

The framework is setup. We have two users with distinct roles able to login to the web application. At this point, running the application allows both users to access the PDF file. This is because Visual Studio’s web server considers any non-application file to be a simple static file. Just like a GIF or JPG, the PDF file may be accessed by any user. We now need to create a custom HTTP handler to intercept requests to the PDF file and check the user’s role.

We can start by creating a FileProtectionHandler class, which implements the interface IHttpHandler. We’ll define the body for ProcessRequest() and check GET requests for access, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class FileProtectionHandler : IHttpHandler
{
...

public void ProcessRequest(HttpContext context)
{
switch (context.Request.HttpMethod)
{
case "GET":
{
// Is the user logged-in?
if (!context.User.Identity.IsAuthenticated)
{
FormsAuthentication.RedirectToLoginPage();
return;
}

string requestedFile = context.Server.MapPath(context.Request.FilePath);

// Verify the user has access to the User role.
if (context.User.IsInRole("User"))
{
SendContentTypeAndFile(context, requestedFile);
}
else
{
// Deny access, redirect to error page or back to login page.
context.Response.Redirect("~/User/AccessDenied.aspx");
}

break;
}
}
}

...

In the above code, we’ll only concerned with looking at GET requests. Remember, this HTTP handler will be defined in the web.config to only process *.pdf requests. Other files will be processed normally by the web server and any other defined modules.

We first check if the user is authenticated via ASP .NET. We can check this in the User.Identity object. If the user is not logged in, we redirect him back to the login page. This will prevent non-logged-in users from accessing the document. Now for users that are logged-in to the web application, we need to check their role. If the user is in the “User” role, we allow access and send them the file. All other users are shown an access denied page. You could also redirect them back to the login page, log the error, or perform any other desirable action.

For users which can access the file, sending the file content is fairly straight-forward. We’ll simply define a SendContentTypeAndFile() method which determines the type of the file request and issues the document.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private HttpContext SendContentTypeAndFile(HttpContext context, String strFile)
{
context.Response.ContentType = GetContentType(strFile);
context.Response.TransmitFile(strFile);
context.Response.End();

return context;
}

private string GetContentType(string filename)
{
// used to set the encoding for the reponse stream
string res = null;
FileInfo fileinfo = new FileInfo(filename);

if (fileinfo.Exists)
{
switch (fileinfo.Extension.Remove(0, 1).ToLower())
{
case "pdf":
{
res = "application/pdf";
break;
}
}

return res;
}

return null;
}

With the custom HTTP handler now defined, we need to plug it into the web application’s web.config to enable it.

Bringing the Custom HTTP Handler to Life

So far, requests to the PDF file will go unchallenged. Both non-logged-in users (depending on the web.config) and authenticated users can access the document since the web server will issue the file upon request. We now need to tell the web server about our custom HTTP handler by adding the settings inside the web.config.

1
2
3
4
<httpHandlers>
...
<add path="*/User/Documents/*.pdf" verb="*" validate="true" type="CustomFileHandlerDemo.Handlers.FileProtectionHandler" />
</httpHandlers>

As shown above, we’ll first define a setting inside the httpHandlers block in the web.config. We’ll add a path of *.pdf inside the User/Documents folder and indicate to process the request with our FileProtectionHandler. This is all Visual Studio’s built-in web server Cassini actually needs to enable our custom HTTP handler. However, IIS will need an extra setting to define the module as well.

1
2
3
4
5
6
7
<system.webServer>
...
<handlers>
<add name="PDF" path="*.pdf" verb="*" type="CustomFileHandlerDemo.Handlers.FileProtectionHandler" resourceType="Unspecified" />
...
</handlers>
</system.webServer>

As shown above, we’ve also defined a handler setting for our FileProtectionHandler, which IIS will register. This will allow you to run the test application in both Visual Studio during development and on IIS when moving to production.

If you now run the test application and login as john, you should be able to access the PDF document as normal. This time, processing goes through the FileProtectionHandler to issue the document to the user. If you try logging in as jane, the file will be denied.

We’ve just successfully created a custom HTTP handler to secure and protect a static PDF file in the C# ASP .NET web application.

Hooking PDF Files Into the Web Application with IIS

It was easy testing the custom HTTP handler in Visual Studio’s built-in web server, Cassini, since all document types are automatically processed in the web application by default. However, IIS needs a few tweaks. IIS will ignore sending requests for static documents, such as PDF files, to the ASP .NET web application and will instead simply serve the request. We need to intercept the request and allow our web application to process it first. To do this, we’ll need to setup an IIS mapping for PDF files (*.pdf), telling IIS to send the request to our web application.

In IIS 5/6

  1. Open the Internet Information Services (IIS) Manager.
  2. For your web application, on the Directory tab, click the Configuration button.
  3. On the Mappings tab of the Application Configuration window, click the Add button to add a new Application Extension Mapping.
  4. In the Executable field, enter: C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll
  5. In the Extension field, enter: *.pdf
  6. Select All Verbs and checkmark Script Engine and Check that file exists.

In IIS 7

  1. Open the Internet Information Services (IIS) Manager.
  2. Open the Handler Mappings setting.
  3. Add a Managed Handler.
  4. For Request Path enter: *.pdf
  5. For Type, select the custom HTTP handler for the application.

A shortcut to this in IIS 7, as mentioned above in the article, is to define the mapping in the web.config within the system.webServer handlers section, as follows:

1
2
3
4
5
6
7
<system.webServer>
...
<handlers>
<add name="PDF" path="*.pdf" verb="*" type="CustomFileHandlerDemo.Handlers.FileProtectionHandler" resourceType="Unspecified" />
...
</handlers>
</system.webServer>

The above code in the web application’s web.config will automatically add the entry into the IIS 7 Handler Mappings section.

The above steps may differ depending on your version of IIS, but should be similar for adding a document mapping to the web application. Once configured, requests for PDF documents will be sent to the web application, where you can process the request before allowing access.

Remember, in Visual Studio’s built-in web server, module mappings are not required, as all requests for files go through the web application, making it easy to test the custom http handler.

Conclusion

A custom HTTP handler can be a powerful method for processing requests to static web application files. By intercepting the requests to the static file or document, a web application can enable advanced processing, such as role and membership based checks on the logged-in user, statistic logging, error control, and more. While Visual Studio’s development environment with Cassini offers an easy way to test custom HTTP handlers, as it sends all document requests to the ASP .NET application, IIS requires additional setup in order to enable the module. IIS 5 and 6 can be configured with an application extension mapping for the document type, while IIS 7 can be configured directly from within the C# ASP .NET web application’s web.config handlers section. In either case, custom handlers are a powerful extension to the ASP .NET web application model and can help your web application create a seamless, professional, and feature-rich experience.

About the Author

This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.

Share