Redirecting Configuration with a Custom Provider

A common issue with Web applications is that applications are managed in various environments. Aside from development environments, you may have test servers, staging servers, live production servers, and potentially warm backup servers. If you store selected Web configuration sections in a central location (such as a central file share or a central configuration database), you have a more manageable solution and, depending on how you implement this, a more secure solution as well.

With ASP.NET 2.0, you can write a custom protected configuration provider that determines information about the current server and the currently running application. A custom provider can reach out to a central repository of configuration information and return the appropriate configuration information. When you have a group of individuals who manage the configuration data for live production servers, it is probably much easier to have such a group manage updates to a single database as opposed to synchronizing configuration files across multiple machines.

Implementing a custom protected configuration provider requires you to derive from the System.Configuration.ProtectedConfigurationProvider class. As you can see, the class signature is very basic:

    public abstract class ProtectedConfigurationProvider : ProviderBase
    {
        public abstract XmlNode Encrypt(XmlNode node);
        public abstract XmlNode Decrypt(XmlNode encryptedNode);
    }

For a sample provider that demonstrates redirecting configuration to a database, you implement only the Decrypt method because this is the method used at runtime to return configuration data to the caller. If you store more complex data inside your protected configuration format, implementing the Encrypt method will make life easier when storing configuration sections in a custom data store.

First look at what a “protected” configuration section in a web.config file will look like using the custom provider:

<membership configProtectionProvider="CustomDatabaseProvider">

     <EncryptedData>
          <sectionInfo name="membership" />
     </EncryptedData>
</membership>

The <membership /> section references a protected configuration provider. Instead of the actual definition of the <membership /> section though, the <EncryptedData /> element is common to all protected configuration sections. However, what is enclosed within this element is determined by each protected configuration provider. In this case, to keep the sample provider very simple, the protected data consists of only a single element: a <sectionInfo /> element.

Unlike protected configuration providers that blindly encrypt and decrypt data, this sample provider needs to know the actual configuration section that is being requested. The custom provider needs to know what section is really being requested because its purpose is to store configuration data in a database for any arbitrary configuration section. The name attribute within the <sectionInfo /> element gives the custom provider the necessary information. Although this is just a basic example of what you can place with <EncryptedData />, you can encapsulate any kind of complex data your configuration provider may need within the XML.

The custom provider will store configuration sections in a database, keying off of a combination of the application’s virtual path and the configuration section. The database schema that follows shows the table structure for storing this:

create table ConfigurationData (
ApplicationName nvarchar(256) NOT NULL,
SectionName nvarchar(150) NOT NULL,
SectionData ntext
)
go
alter table ConfigurationData
  add constraint PKConfigurationData
  PRIMARY KEY (ApplicationName,SectionName)
Go

Retrieving this information will similarly be very basic with just a single stored procedure pulling back the SectionData column that contains the raw text of the requested configuration section:

create procedure RetrieveConfigurationSection
  @pApplicationName nvarchar(256),
  @pSectionName nvarchar(256)
as
select SectionData
from   ConfigurationData
where  ApplicationName  = @pApplicationName
and    SectionName      = @pSectionName
go

Because the custom protected configuration provider needs to connect to a database, a connection string must be included within the definition of the provider.

<configProtectedData>
  <providers>
 <add name="CustomDatabaseProvider"
      type="CustomProviders.DatabaseProtectedConfigProvider,
	  	CustomProviders"
      connectionStringName="ConfigurationDatabase"
   />
 </providers>
</configProtectedData>

The sample provider configuration looks similar to the configurations for the RSA and DPAPI protected configuration providers that ship in the 2.0 Framework. In this case, however, the custom provider requires a connectionStringName element so that it knows which database and database server to connect to. The value of this attribute is simply a reference to a named connection string in the <connectionStrings /> section, as shown here:

<connectionStrings>
     <add name="ConfigurationDatabase"
          connectionString="server=.;Integrated _
              Security=true;database=CustomProtectedConfiguration"/>
</connectionStrings>

When creating your own custom providers, you have the freedom to place any provider-specific information you deem necessary in the provider’s <add /> element.

Now that you have seen the data structure and configuration related information, take a look at the code for the custom provider. Because a protected configuration provider ultimately derives from System.Configuration.Provider.ProviderBase, the custom provider can override portions of ProviderBase as well as ProtectedConfigurationProvider. The custom provider overrides ProviderBase.Initialize so that the provider can retrieve the connection string from configuration:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Configuration.Provider;
using System.Web;
using System.Web.Hosting;
using System.Web.Configuration;
using System.Xml;
namespace CustomProviders
{
  public class
 DatabaseProtectedConfigProvider : ProtectedConfigurationProvider
  {
    private string connectionString;
    public DatabaseProtectedConfigProvider() { }
    public override void Initialize(string name,
        System.Collections.Specialized.NameValueCollection config)
    {
        string connectionStringName = config["connectionStringName"];
        if (String.IsNullOrEmpty(connectionStringName))
            throw new ProviderException("You must specify " +
                   "connectionStringName in the provider configuration");
        connectionString =
      WebConfigurationManager.ConnectionStrings[connectionStringName] _
            .ConnectionString;
        if (String.IsNullOrEmpty(connectionString))
            throw new ProviderException("The connection string " +
              "could not be found in <connectionString />.");
        config.Remove("connectionStringName");
        base.Initialize(name, config);
      }
    //Remainder of provider implementation
    }
}

The processing inside of the Initialize method performs a few sanity checks to ensure that the connectionStringName attribute was specified in the provider’s <add /> element, and that furthermore the name actually points at a valid connection string. After the connection string is obtained from the ConnectionStrings collection, it is cached internally in a private variable.

Of course, the interesting part of the provider is its implementation of the Decrypt method:

public override XmlNode Decrypt(XmlNode encryptedNode)
{
  //Application name
  string applicationName = HostingEnvironment.ApplicationVirtualPath;
  XmlNode xn =
    encryptedNode.SelectSingleNode("/EncryptedData/sectionInfo");
  //Determine the configuration section to retrieve from the database
  string sectionName = xn.Attributes["name"].Value;
  using (SqlConnection conn = new SqlConnection(connectionString))
  {
      SqlCommand cmd =
          new SqlCommand("RetrieveConfigurationSection", conn);
      cmd.CommandType = CommandType.StoredProcedure;
      SqlParameter p1 =
	   new SqlParameter("@pApplicationName", applicationName);
      SqlParameter p2 = new SqlParameter("@pSectionName", sectionName);
      cmd.Parameters.AddRange(new SqlParameter[] { p1, p2 });
      conn.Open();
      string rawConfigText = (string)cmd.ExecuteScalar();
      conn.Close();
      //Convert the string information from the database into an XmlNode
      XmlDocument xd = new XmlDocument();
      xd.LoadXml(rawConfigText);
      return xd.DocumentElement;
  }
}

The Decrypt method’s purpose is take information about the current application and information available from the <sectionInfo /> element and use it to retrieve the correct configuration data from the database.

The provider determines the correct application name by using the System.Web.Hosting.HostingEnvironment class to determine the current application’s virtual path. The name of the configuration section to retrieve is determined by parsing the <EncryptedData /> section to get to the name attribute of the custom <sectionInfo /> element. With these pieces of data the provider connects to the database using the connection string supplied by the provider’s configuration section.

The configuration data stored in the database is just the raw XML fragment for a given configuration section. For this example, which stores a <membership /> section in the database, the database table just contains the text of the section’s definition taken from machine.config stored in an ntext field in SQL Server (i.e. just copy the <membership /> section from machine.config and insert it into the sample database table’s SectionData column). Because protected configuration providers work in terms of XmlNode instances, and not raw strings, the provider converts the raw text in the database back into an XmlDocument, which can then be subsequently returned as an XmlNode instance. Because the data in the database is well-formed XML, the provider can just return the DocumentElement for the XmlDocument.

What is really powerful about custom protected configuration providers is that the Framework’s configuration system is oblivious to how the configuration information is obtained. For example the following configuration code works unchanged even though the <membership /> section is now being retrieved from a database.

MembershipSection ms =
(MembershipSection)ConfigurationManager.GetSection(
	"system.web/membership");

This is exactly what you would want from protected configuration. Nothing in the application code changes despite the fact that now the configuration section is stored remotely in a database as opposed to locally on the file system.

One caveat to keep in mind with custom protected configuration providers is that after the data is physically stored outside of a configuration file, ASP.NET is no longer able to automatically trigger an app-domain restart whenever the configuration data changes. With the built-in RSA and DPAPI configuration providers, this isn’t an issue because the encrypted text is still stored in web.config and machine.config files. ASP.NET listens for change notifications and triggers an app-domain restart in the event the encrypted sections in any of these files change.

However, ASP.NET does not have a facility to trigger changes based on protected configuration data stored in other locations. For this reason, if you do write a custom provider along the lines of the sample provider, you need to incorporate operational procedures that force app-domains to recycle whenever you update configuration data stored in locations other than the standard file-based configuration files.

About the Author

Stefan Schackow currently works as a program manager at Microsoft on the ASP.NET product team. He has worked extensively with the new application services delivered in ASP.NET 2.0, including Membership and Role Manager. Prior to joining the ASP.NET product team, he worked in Microsoft’s consulting services designing Web and database applications for various enterprise clients.


This article is adapted from Professional ASP.NET 2.0 Security, Membership, and Role Management by Stefan Schackow (Wrox, 2006, ISBN: 0-7645-9698-5), from Chapter 4 “Configuration System Security.” Used here with permission of Wiley Publishing.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read