How to Take Advantage of the IIS 7.0 Integrated Pipeline

By Mike Volodarsky

December 5, 2007

Introduction

IIS 6.0 and previous versions allowed the development of .NET application components via the ASP.NET platform. ASP.NET integrated with IIS via an ISAPI extension, and exposed its own application and request processing model. This effectively exposed two separate server pipelines, one for native ISAPI filters and extension components, and another for managed application components. ASP.NET components would execute entirely inside the ASP.NET ISAPI extension bubble and only for requests mapped to ASP.NET in the IIS script map configuration.

IIS 7.0 and above integrates the ASP.NET runtime with the core web server, providing a unified request processing pipeline that is exposed to both native and managed components known as modules. The many benefits of integration include:

  • Allowing services provided by both native and managed modules to apply to all requests, regardless of handler. For example, managed Forms Authentication can be used for all content, including ASP pages, CGIs, and static files.
  • Empowering ASP.NET components to provide functionality that was previously unavailable to them due to their placement in the server pipeline. For example, a managed module providing request rewriting functionality can rewrite the request prior to any server processing, including authentication.
  • A single place to implement, configure, monitor and support server features such as single module and handler mapping configuration, single custom errors configuration, single url authorization configuration.

This article examines how the ASP.NET applications can take advantage of the integrated mode in IIS 7.0 and above, and illustrates the following tasks:

  • Enabling/disabling modules on a per-application level.
  • Adding managed application modules to the server, and enabling them to apply to all request types.
  • Adding managed handlers.

Learn more about building IIS 7.0 and above modules in Developing IIS 7.0 and Above Modules and Handlers with the .NET Framework.

See also the blog, http://www.mvolo.com/, for more tips on taking advantage of Integrated mode and developing IIS modules that leverage the ASP.NET integration in IIS 7.0 and above. There, download a number of such modules including Redirecting requests to your application with the HttpRedirection module, Nice looking directory listings for your IIS website with DirectoryListingModule, Displaying pretty file icons in your ASP.NET applications with IconHandler, and Stopping hot-linking with IIS and ASP.NET.

Prerequisites

To follow the steps in this document, the following IIS 7.0 and above features must be installed.

ASP.NET

Install ASP.NET via the Windows Vista Control Panel. Select "Programs and Features" - "Turn on or off Windows features". Then open "Internet Information Services" - "World Wide Web Services" - "Application Development Features" and check "ASP.NET".

If you have a Windows Server® 2008 build, open "Server Manager" - "Roles" and select "Web Server (IIS)". Click "Add role services". Under "Application Development," check "ASP.NET".

Classic ASP

We want to show how ASP.NET modules now work with all content and not just ASP.NET pages, so install classic ASP via the Windows Vista Control Panel. Select "Programs" - "Turn on or off Windows features". Then open "Internet Information Services" - "World Wide Web Services" - "Application Development Features" and check "ASP".

If you have a Windows Server 2008 build, open "Server Manager" - "Roles" and select "Web Server (IIS)". Click "Add role services". Under "Application Development," check "ASP".

Adding Forms Authentication to Your Application

As part of this task, we enable the ASP.NET Forms-based Authentication for the application. In the next task, we enable the Forms Authentication module to run for all requests to your application, regardless of content type.

First, configure forms authentication as you would for a normal ASP.NET application.

Creating a Sample Page

To illustrate the feature, we add a default.aspx page to the web root directory. Open notepad (to make sure you have access to the wwwroot directory below, you must run as administrator--right click on Programs\Accessories\Notepad icon, and click "Run as administrator"), and create the following file: %systemdrive%\inetpub\wwwroot\default.aspx. Paste the following lines into it:

<%=Datetime.Now%>
<BR>
Login Name: <asp:LoginName runat="server"/>

All default.aspx does is display the current time and the name of the logged in user. We use this page later to show forms authentication in action.

Configuring Forms Authentication and Access Control Rules

Now, to protect default.aspx with forms authentication. Create a web.config file in the %systemdrive%\inetpub\wwwroot directory and add the configuration shown below:

<configuration>
<system.web>
<!--membership provider entry goes here-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
<authentication mode="Forms"/>
</system.web>
</configuration>

This configuration sets the ASP.NET authentication mode to use forms-based authentication, and adds authorization settings to control access to the application. These setting deny access to anonymous users (?) and only allow authenticated users (*).

Creating a Membership Provider

Step 1: We must provide an authentication store against which the user credentials will be verified. To illustrate the deep integration between ASP.NET and IIS 7.0 and above, we use our own XML-based Membership provider (you can also use the default SQL Server Membership Provider if SQL Server is installed).

Add the following entry right after the initial <configuration>/<system.web> configuration element in the web.config file:

<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider">
<providers>
<add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/>
</providers>
</membership>

Step 2: After the configuration entry is added, you must save the Membership provider code provided in Appendix as XmlMembershipProvider.cs in your %systemdrive%\inetpub\wwwroot\App_Code directory. If this directory does not exist, you must create it.

Note: If using Notepad, be sure to set Save As: All Files to prevent the file from being saved as XmlMembershipProvider.cs.txt.

Step 3: All that remains is the actual credential store. Save the xml snippet below as MembershipUsers.xml file in the %systemdrive%\inetpub\wwwroot\App_Data directory.

Note: If using Notepad, be sure to set Save As: All Files to prevent the file from being saved as MembershipUsers.xml.txt.

<Users>
<User>
<UserName>Bob</UserName>
<Email>bob@contoso.com</Email>
</User>
<User>
<UserName>Alice</UserName>
<Password>contoso!</Password>
<Email>alice@contoso.com</Email>
</User>
</Users>

If the App_Data directory does not exist, you must create it.

Note: Due to security changes in Windows Server 2003 and Windows Vista SP1, you can no longer use the IIS Administration tool to create Membership user accounts for non-GACed Membership providers.

After completing this task, go to the IIS Administration tool and add or delete users for your application. Start "INETMGR" from the "Run…" menu. Open the "+" signs in the tree view on the left until the "Default Web Site" displays. Select "Default Web Site" and then move to the right and click the "Security" category. The remaining features show ".NET Users". Click ".NET Users" and add one or more user accounts of your choice.

Look in MembershipUsers.xml to find the newly created users.

Creating a Login Page

In order to use forms authentication, we must to create a login page. Open notepad (To make sure you have access to the wwwroot directory below, you need to run as administrator by right clicking on Programs\Accessories\Notepad icon, and clicking "Run as administrator"), and create the login.aspx file in the %systemdrive%\inetpub\wwwroot directory. Note - be sure to set Save As: All Files to prevent the file from being saved as login.aspx.txt. Paste the following lines into it:

<%@ Page language="c#" %>
<form id="Form1" runat="server">
<asp:LoginStatus runat="server" />
<asp:Login runat="server" />
</form>

This is the login page to which you are redirected when your authorization rules deny access to a particular resource.

Testing

Open an Internet Explorer Window and request http://localhost/default.aspx. You see that you are redirected to login.aspx, because initially your were not authenticated, and we withheld access to unauthenticated users earlier. If you successfully log in with one of the username/password pairs specified in MembershipUsers.xml, you get redirected back to default.aspx page originally requested. This page then shows the current time and the user identity with which you authenticated.

At this point, we have successfully deployed a custom authentication solution using Forms Authentication, Login controls, and Membership. This functionality is not new in IIS 7.0 or above – it has been available since ASP.NET 2.0 on previous IIS releases.

However, the problem is that only content handled by ASP.NET is protected.

If you close and re-open the browser window, and request http://localhost/iisstart.htm, you are not prompted for credentials. ASP.NET does not participate in a request for a static file like iisstart.htm. Therefore, it cannot protect it with forms authentication. You see the same behavior with classic ASP pages, CGI programs, PHP or Perl scripts. Forms authentication is an ASP.NET feature, and simply is not available during requests to those resources.

Enabling Forms Authentication for the Entire Application

In this task, we eliminate the limitation of ASP.NET on previous releases, and enable the ASP.NET Forms Authentication and Url Authorization functionality for the entire application.

In order to take advantage of ASP.NET integration, our application must be configured to run in Integrated mode. The ASP.NET integration mode is configurable per application pool, enabling ASP.NET applications in different modes to be hosted side by side on the same server. The default application pool in which our application lives already uses Integrated mode by default, so we do not need to do anything here.

So, why did we fail to experience the benefits of Integrated mode when we tried to access the static page earlier? The answer lies in the default settings for all ASP.NET modules shipped with IIS 7.0 and above.

Taking Advantage of the Integrated Pipeline

The default configuration for all managed modules shipped with IIS 7.0 and above, including the Forms Authentication and URL Authorization modules, uses a precondition so that these modules only apply to content that an (ASP.NET) handler manages. This is done for backwards compatibility reasons.

By removing the precondition, we make the desired managed module execute for all requests to the application, regardless of content. This is necessary in order to protect our static files, and any other application content with Forms-based authentication.

To do this, open the application's web.config file located in the %systemdrive%\inetpub\wwwroot directory, and paste the following lines immediately below the first <configuration> element:

<system.webServer>
<modules>
<remove name="FormsAuthenticationModule" />
<add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />
<remove name="UrlAuthorization" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
<remove name="DefaultAuthentication" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" />
</modules>
</system.webServer>

This configuration re-adds the module elements without the precondition, enabling them to execute for all requests to the application.

Testing

Close all instances of Internet Explorer so that the credentials entered before are no longer cached. Open Internet Explorer, and make a request to the application at the following URL:

http://localhost/iisstart.htm

You are redirected to the login.aspx page in order to log in.

Log in with a username/password pair used previously. When you successfully log in, you are redirected back to the original resource, which displays the IIS welcome page.

Note: Even though you requested a static file, the managed forms authentication module and the URL authorization module provided their services in order to protect your resource.

To illustrate this even further, we add a classic ASP page and protect it with Forms Authentication.

Open notepad (to make sure you have access to the wwwroot directory below, you must run as administrator--right click on Programs\Accessories\Notepad icon, and click "Run as administrator"), and create a page.asp file in your %systemdrive%\inetpub\wwwroot directory.

Note: If using Notepad, be sure to set Save As: All Files to prevent the file from being saved as page.asp.txt. Paste the lines below into it:

<%
for each s in Request.ServerVariables
Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>

Close all Internet Explorer instances again-- otherwise, your credentials are still cached and request http://localhost/page.asp. You are again redirected to the login page, and after successful authentication, display the ASP page.

Congratulations – you have successfully added managed services to the server, enabling them for all requests to the server regardless of handler!

Summary

This walkthrough demonstrated how the ASP.NET Integrated mode can be leveraged to make powerful ASP.NET features available to not just ASP.NET pages, but to the entire application.

More importantly, you can now build new managed modules using the familiar ASP.NET 2.0 APIs that have the ability to execute for all application content, and provided an enhanced set of request processing services to your application.

Feel free to check out the blog, http://www.mvolo.com/, for more tips on taking advantage of Integrated mode and developing IIS modules that leverage the ASP.NET integration in IIS 7 and above. There, you can also download a number of such modules including Redirecting requests to your application with the HttpRedirection module, Nice looking directory listings for your IIS website with DirectoryListingModule, Displaying pretty file icons in your ASP.NET applications with IconHandler, and Stopping hot-linking with IIS and ASP.NET.

Appendix

This Membership provider is based on the sample XML membership provider found in this MSDN article.

To use this Membership provider, save the code as XmlMembershipProvider.cs in your %systemdrive%\inetpub\wwwroot\App_Code directory. If this directory doesn't exist you will have to create it. Note - be sure to set Save As: All Files if using Notepad to prevent the file from being saved as XmlMembershipProvider.cs.txt.

Note: This membership provider sample is for the purposes of this demo only. It does not conform to the best practices and security requirements for a production Membership provider, including storing passwords securely and auditing user actions. Do not use this membership provider in your application!

using System;
using System.Xml;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
using System.Web.Hosting;
using System.Web.Management;
using System.Security.Permissions;
using System.Web;

public class AspNetReadOnlyXmlMembershipProvider : MembershipProvider
{
private Dictionary<string, MembershipUser> _Users;
private string _XmlFileName;
// MembershipProvider Properties

public override string ApplicationName
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public override bool EnablePasswordRetrieval
{
get { return false; }
}

public override bool EnablePasswordReset
{
get { return false; }
}

public override int MaxInvalidPasswordAttempts
{
get { throw new NotSupportedException(); }
}

public override int MinRequiredNonAlphanumericCharacters
{
get { throw new NotSupportedException(); }
}

public override int MinRequiredPasswordLength
{
get { throw new NotSupportedException(); }
}

public override int PasswordAttemptWindow
{
get { throw new NotSupportedException(); }
}

public override MembershipPasswordFormat PasswordFormat
{
get { throw new NotSupportedException(); }
}

public override string PasswordStrengthRegularExpression
{
get { throw new NotSupportedException(); }
}

public override bool RequiresQuestionAndAnswer
{
get { return false; }
}

public override bool RequiresUniqueEmail
{
get { throw new NotSupportedException(); }
}

// MembershipProvider Methods

public override void Initialize(string name,
NameValueCollection config)
{
// Verify that config isn't null
if (config == null)
throw new ArgumentNullException("config");

// Assign the provider a default name if it doesn't have one
if (String.IsNullOrEmpty(name))
name = "ReadOnlyXmlMembershipProvider";

// Add a default "description" attribute to config if the
// attribute doesn't exist or is empty
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description",
"Read-only XML membership provider");
}

// Call the base class's Initialize method
base.Initialize(name, config);

// Initialize _XmlFileName and make sure the path
// is app-relative
string path = config["xmlFileName"];

if (String.IsNullOrEmpty(path))
path = "~/App_Data/MembershipUsers.xml";

if (!VirtualPathUtility.IsAppRelative(path))
throw new ArgumentException
("xmlFileName must be app-relative");

string fullyQualifiedPath = VirtualPathUtility.Combine
(VirtualPathUtility.AppendTrailingSlash
(HttpRuntime.AppDomainAppVirtualPath), path);

_XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath);
config.Remove("xmlFileName");

// Make sure we have permission to read the XML data source and
// throw an exception if we don't
FileIOPermission permission =
new FileIOPermission(FileIOPermissionAccess.Read,
_XmlFileName);
permission.Demand();

// Throw an exception if unrecognized attributes remain
if (config.Count > 0)
{
string attr = config.GetKey(0);
if (!String.IsNullOrEmpty(attr))
throw new ProviderException
("Unrecognized attribute: " + attr);
}
}

public override bool ValidateUser(string username, string password)
{
// Validate input parameters
if (String.IsNullOrEmpty(username) ||
String.IsNullOrEmpty(password))
return false;

// Make sure the data source has been loaded
ReadMembershipDataStore();

// Validate the user name and password
MembershipUser user;

if (_Users.TryGetValue(username, out user))
{
if (user.Comment == password) // Case-sensitive
{
return true;
}
}

return false;
}

public override MembershipUser GetUser(string username,
bool userIsOnline)
{

// Note: This implementation ignores userIsOnline
// Validate input parameters

if (String.IsNullOrEmpty(username))
return null;

// Make sure the data source has been loaded
ReadMembershipDataStore();

// Retrieve the user from the data source
MembershipUser user;

if (_Users.TryGetValue(username, out user))
return user;

return null;
}

public override MembershipUserCollection GetAllUsers(int pageIndex,
int pageSize, out int totalRecords)
{
// Note: This implementation ignores pageIndex and pageSize,
// and it doesn't sort the MembershipUser objects returned
// Make sure the data source has been loaded

ReadMembershipDataStore();

MembershipUserCollection users =
new MembershipUserCollection();

foreach (KeyValuePair<string, MembershipUser> pair in _Users)
users.Add(pair.Value);

totalRecords = users.Count;

return users;
}

public override int GetNumberOfUsersOnline()
{
throw new NotSupportedException();
}

public override bool ChangePassword(string username,
string oldPassword, string newPassword)
{
throw new NotSupportedException();
}

public override bool
ChangePasswordQuestionAndAnswer(string username,
string password, string newPasswordQuestion,
string newPasswordAnswer)
{
throw new NotSupportedException();
}

public override MembershipUser CreateUser(string username,
string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved, object providerUserKey,
out MembershipCreateStatus status)
{
throw new NotSupportedException();
}

public override bool DeleteUser(string username,
bool deleteAllRelatedData)
{
throw new NotSupportedException();
}

public override MembershipUserCollection
FindUsersByEmail(string emailToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}

public override MembershipUserCollection
FindUsersByName(string usernameToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}

public override string GetPassword(string username, string answer)
{
throw new NotSupportedException();
}

public override MembershipUser GetUser(object providerUserKey,
bool userIsOnline)
{
throw new NotSupportedException();
}

public override string GetUserNameByEmail(string email)
{
throw new NotSupportedException();
}

public override string ResetPassword(string username,
string answer)

{
throw new NotSupportedException();
}

public override bool UnlockUser(string userName)
{
throw new NotSupportedException();
}

public override void UpdateUser(MembershipUser user)
{
throw new NotSupportedException();

}

// Helper method

private void ReadMembershipDataStore()
{
lock (this)
{
if (_Users == null)
{
_Users = new Dictionary<string, MembershipUser>
(16, StringComparer.InvariantCultureIgnoreCase);
XmlDocument doc = new XmlDocument();
doc.Load(_XmlFileName);
XmlNodeList nodes = doc.GetElementsByTagName("User");

foreach (XmlNode node in nodes)
{
MembershipUser user = new MembershipUser(
Name, // Provider name
node["UserName"].InnerText, // Username
null, // providerUserKey
node["Email"].InnerText, // Email
String.Empty, // passwordQuestion
node["Password"].InnerText, // Comment
true, // isApproved
false, // isLockedOut
DateTime.Now, // creationDate
DateTime.Now, // lastLoginDate
DateTime.Now, // lastActivityDate
DateTime.Now, // lastPasswordChangedDate
new DateTime(1980, 1, 1) // lastLockoutDate
);

_Users.Add(user.UserName, user);

}
}
}
}
}