Introduction
Migrating a web application from another technology to ASP.NET requires careful planning. Introducing Iron Speed Designer into the mix adds even more decisions to consider. Usability and security strategies must be formulated before proceeding; otherwise, you may face the prospect of reworking code instead of reworking your strategy.
Our organization’s legacy web application is an internal (intranet) app written in ColdFusion on the front end and using Microsoft SQL Server as its database. One of the first tasks I faced, as our organization’s application architect, was to decide upon the technology platform to succeed the current platform. I settled on ASP. NET/C#, which surprised even me, coming from a background of six years in the J2EE world (but that’s another story).
The size and complexity of the legacy application ruled out a "big bang" approach to conversion. Instead of "conversion", I adopted a strategy of "migration". In this approach, new user requirements are implemented in ASP.NET pages generated using Iron Speed Designer wherever possible. The legacy application is "extended" by linking new ASP.NET pages to it. This is made more manageable in that we continue to use the same underlying database as the ColdFusion application. Existing pages will be converted whenever an opportunity arises.
The legacy application utilizes a "Forms" authentication/authorization model. Consequently, we already had user, role, and user/role tables in the database. So, my first decision was whether to:
- Use Iron Speed Designer’s security model, or
- Use ASP.NET 2.0’s security model, or
- Adapt the existing application’s security mechanism in C#.
Regardless of the approach, I knew that I wanted to leverage the existing security data (users, roles, and assignments). In the end, I chose to use the ASP.NET 2.0 security model. The main reason for this was that I also intend to use third party controls. It’s often desirable for controls, such as menus, to be "security aware" in order to suppress options from users who aren’t members of certain roles. The controls I planned on using (both third party and native ASP.NET) are aware of ASP.NET 2.0 security, and so my decision was made for me.
A big hurdle I had to overcome was allowing users to access the new pages without requiring them to log in. Essentially, the ASP.NET environment needed to detect un-authenticated requests, determine the identity of the user making the request, and then automatically log them in to the Forms security mechanism.
Another requirement was that the application should use a SQL login for database connections. Further, I wanted to minimize or eliminate the manipulation (adding/changing/deleting) of web.config elements by administrators as they move the application from environment to environment. In other words, as an application moves from development to test to production environments, the web.config file should not require modification to connect to that environment’s database. Finally, we wanted to hide the SQL login accounts and passwords from everybody, even developers.
Procedure
With these requirements in mind, we developed the following approach, and it has worked quite well:
- Configure the web application to use impersonation. This is done by setting the identity section of the web.config file as shown in figure 1 below. Impersonation is an IIS mode of operation in which the web application code executes within the security context of the Windows user making the request.
- Configure the web application to use Forms security. This is done in the
authentication
section of the web.config file, as shown in figure 2 below.
- Configure the web application to use a custom membership provider. This is done in the
membership
section of the web.config file as shown in figure 3 below.
- Configure the web application to use a custom role provider. This is done in the
roleManager
section of the web.config file as shown in figure 4 below.
- Configure the web application to reject un-authenticated access to all pages. This is done in the authorization section of the web.config file as shown in figure 5 below.
- Write a custom login page to automatically login in users based on their Windows identity. See figure 6 below.
- Write the custom membership provider class. See figure 7 below.
- Write the custom role provider class. See figure 8 below.
- Utilize machine.config and encryption using aspnet_regiis.exe to protect login ID and password information and minimize manipulation of *.config files as the application is moved between development, test, and production environments. See Working With machine.config below.
<!---->
<identity impersonate="true"/>
Figure 1 - Impersonation causes the web application to execute within the security context of the Windows domain user. When John Doe in accounting requests a page, the code executes on the server using the credentials of the Windows user MYDOMAIN\DoeJohn.
<!---->
<authentication mode="Forms">
<forms name="authCookie"
loginUrl="Common/Login.aspx" protection="All" path="/" />
</authentication>
Figure 2 - The other options available are Windows and Passport. We’ve elected Forms security to leverage our legacy application’s security database.
<membership defaultProvider="MyMembershipProvider"
userIsOnlineTimeWindow="99">
<providers>
<clear/>
<add name="MyembershipProvider"
type="Fund.FMS.MyMembershipProvider"
connectionStringName="MyConnectionString"
enablePasswordRetrieval="false"
enablePasswordReset="false"
requiresQuestionAndAnswer="false"
writeExceptionsToEventLog="true"/>
</providers>
</membership>
Figure 3 - The membership
section allows us to define the class that will handle the authentication of user IDs and passwords. We are overriding the default class (SqlMembershipProvider
) and supplying our own.
<roleManager
defaultProvider="MyRoleProvider"
enabled="true"
cacheRolesInCookie="true"
cookieName=".ASPROLES"
cookieTimeout="30"
cookiePath="/"
cookieRequireSSL="false"
cookieSlidingExpiration="true"
cookieProtection="All">
<providers>
<clear/>
<add
name="MyRoleProvider"
type="Fund.FMS.MyRoleProvider"
connectionStringName="MyConnectionString"
applicationName="FMS"
writeExceptionsToEventLog="false"/>
</providers>
</roleManager>
Figure 4 – The roleManager
section allows us to define the class that will handle the authorization of duties within our application, specifically, supplying the role membership of a given user. We are overriding the default class (SqlRoleProvider
) and supplying our own.
<!---->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
Figure 5 - The authorization
section allows us to specify that all pages require authorization, regardless of who is making the request.
protected void Page_Load(object sender, EventArgs e)
{
bool bSuccess = false;
string errMessage = "Login failed.";
WindowsIdentity ident = WindowsIdentity.GetCurrent();
if (ident == null)
return;
string userId = ident.Name.Replace("MYDOMAIN\\", ""); // remove domain name
string password = "";
/* Get the connection string info from web.config
by using the Configuration class*/
Configuration cfg = WebConfigurationManager.OpenWebConfiguration(
System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
ConnectionStringSettingsCollection connectionStrings =
cfg.ConnectionStrings.ConnectionStrings;
ConnectionStringSettings connString = (ConnectionStringSettings)
connectionStrings["MyConnectionString"];
if (connString == null)
{
WriteToEventLog(new Exception("A configuration entry for connection string " +
"'MyConnectionString' was not found."), "Exit");
throw new Exception("A failure has occurred.");
}
try
{
SqlConnection conn = new SqlConnection(connString.ConnectionString);
conn.Open();
SqlCommand command = conn.CreateCommand();
command.CommandText =
"select password, fst_nme, lst_nme from usr_tbl where username = @username";
SqlParameter parm = new SqlParameter("@username", userId);
command.Parameters.Add(parm);
SqlDataReader reader = command.ExecuteReader();
if (reader.Read())
{ password = reader.GetString(reader.GetOrdinal("password"));
reader.Close();
command.Dispose();
conn.Close();
try
{
// using current user and password retrieved from legacy security table,
// log into the ASP.NET 2.0 Forms security manager.
if (Membership.ValidateUser(userId, password))
{
FormsAuthentication.RedirectFromLoginPage(userId, false);
}
}
catch (System.Threading.ThreadAbortException e1)
{
// the RedirecFromLoginPage throws a ThreadAbortException
// by design, so we just catch it and eat it...
}
catch (System.Exception e2)
{
}
}
else
{
reader.Close();
command.Dispose();
conn.Close();
}
}
catch (Exception e2)
{
}
Figure 6 - This custom login page retrieves the Windows identity of the person making the request, which is made possible by having impersonation enabled. Using the Windows user ID, we retrieve the password from the legacy database and attempt to programmatically login to our custom Membership provider. If the login fails or there are any exceptions, we simply continue on, resulting in a standard forms login page being rendered to the user.
using System.Web.Security;
using System.Configuration.Provider;
using System.Collections.Specialized;
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Diagnostics;
using System.Web;
using Systelobalization;
using System.Security.Cryptography;
using System.Text;
using System.Web.Configuration;
namespace Fund.FMS
{
public sealed class MyMembershipProvider : MembershipProvider
{
public override bool ValidateUser(string username, string password)
{
}
}
Figure 7 – This is the declaration of the custom membership provider, called MyMembershipProvider
. It extends the base class MembershipProvider
, which is part of the .NET framework itself. You simply override the methods, such as ValidateUser
, reading from your own security tables. In your code, you would refer to this via an interface, either explicitly, or indirectly, such as through the current page’s Membership
property, which references the instance loaded as a result of the specification in the web.config file.
I have located this class in the AppCode directory of the Iron Speed application.
Detailed instructions on how to implement a custom membership provider are available on MSDN.
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using System.Configuration.Provider;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
namespace Fund.FMS
{
public sealed class MyRoleProvider : RoleProvider
{
public public override string[] GetRolesForUser(string username)
{
}
}
Figure 8 - This is the declaration of the custom role provider, called MyRoleProvider
. It extends the base class RoleProvider
, which is part of the .NET framework itself. You simply override the methods, such as IsUserInRole
, reading from your own security tables. In your code, you would refer to this via an interface, either explicitly, or indirectly, such as through the current page’s Roles
property, which references the role provider, which references the instance loaded as a result of the specification in the web.config file.
I have located this class in the AppCode directory of the Iron Speed application.
Detailed instructions on how to implement a custom role provider are available on MSDN.
Working With machine.config
Iron Speed Designer stores database connection information in connection string format, but stores the key name and value in the appSettings
section of the file instead of the connectionStrings
section. Personally, I’d like to see this changed, but this is how it is at the present time.
I also had to create a connection string with essentially the same information for our custom role and membership providers, as one of the required configuration elements is the name of the connection strin. which the provider uses for its database connections.
As mentioned earlier, it is undesirable to have to modify the connection string values in web.config as an application moves from development to test to production environments. The file machine.config contains configuration information which supplements the configuration information found in web.config. In other words, you can define a connection string in machine.config instead of web.config. If you define the same element in both places, you may receive a run-time error telling you the configuration element has been defined more than once.
As you might infer from the name, machine.config contains values particular to the machine on which it resides. Thus, a connection string can be created in the machine.config files of each server in your environment. For example, the machine.config on the developer’s desktop could contain a connection string pointing to a local database. The machine.config file on the test machine could contain the same connection string but point to the database server associated with the test environment. The same would apply to the production machine.
The machine.config file is located in the CONFIG directory of your .NET install directory, typically, C:\Windows\Microsoft.NET\Framework\v2.0.50727 (or whatever version you’ve installed). At run-time, configuration data from machine.config is combined with configuration data from web.config to provide a complete set of configuration data to your web application.
An additional step is required to incorporate machine.config into our strategy when dealing with Iron Speed Designer -generated apps. The problem is that Iron Speed Designer does not read the machine.config and web.config like the ASP.NET runtime. It only reads web.config. Thus, you must leave the connection string in web.config, which Iron Speed Designer actually stores in the appSettings
section of the file. If you remove the Iron Speed Designer -generated connection string from the appSettings
section, Iron Speed Designer will complain that it cannot find your application’s connection string.
The workaround for this is that when we deploy a project to our test environment, we remove, rename, or comment out the Iron Speed Designer -generated connection string. This will prevent the ASP.NET runtime from finding two connection strings with the same name (one from web.config and one from machine.config). With regard to our connection string used by the custom providers, it can be removed completely from the web.config file as Iron Speed Designer doesn’t know or care about it. Thus, we need only define it in machine.config.
We can now deploy updated versions of the application, and the only modification that needs to be done to the web.config file is that the developer removes, renames, or comments out the Iron Speed Designer-generated connection string when the application is moved from development to test. No modifications are required at all when moving from test to production, as the Iron Speed Designer connection string has already been renamed, removed, or commented out of web.config and is defined in machine.config. At this point, we have machine.config files on developer desktops (development), plus the test and production servers. Recall that we use SQL logins and want to prevent developers from knowing the passwords to the logins. To accomplish this, we encrypt the machine.config files.
The utility aspnet_regiis.exe allows you to encrypt and decrypt sections of both web.config and machine.config files. The ASP.NET runtime will decrypt the contents on-the-fly when the application is running. The two commands shown in Figure 9 below encrypt the connectionStrings
and appSettings
sections of the machine.config file.
aspnet_regiis.exe -pd "connectionStrings. -pkm -prov "DataProtectionConfigurationProvider"
aspnet_regiis.exe -pd "appSettings. -pkm -prov "DataProtectionConfigurationProvider"
Figure 9 - Using aspnet_regiis to encrypt machine.config
The –pkm option tells aspnet_regiis.exe to encrypt the specified section in the machine.config file. Omitting the option –pkm would encrypt a web.config file.
Run this command from C:\Windows\Microsoft.NET\Framework\v2.0.50727 (or the directory for your version of .NET).
As a final measure of protection, you can configure Microsoft IIS to not allow debugging on the test and production servers. This prevents curious developers from stepping through the code and inspecting the decrypted connection string with the debugger.
Conclusion
In this article, we’ve implemented strategies that allow us to seamlessly integrate ASP.NET pages into an existing web application. Additionally, we’ve seen how to override the default ASP.NET 2.0 Forms mechanism with our own, leveraging the legacy security data – all of this without requiring the user to log into the ASP.NET environment. Finally, we’ve covered how to use the machine.config file to provide environment specific configuration data, and how to incorporate this approach into an Iron Speed Designer-generated application, as well as how to protect SQL login information.