|
|
|
|
| Rewriting a Legacy Application with Iron Speed Designer |
|
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".
- Joe Meirow, Project Manager and System Architect, Michigan Conference of Teamsters Welfare Fund
August 16, 2006
Iron Speed Designer V4.0
|
|
| 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 uses 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 to 1) use Iron Speed Designer’s
security model or 2) use ASP.NET 2.0’s security model or 3) 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 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:
1) 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.
2) 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.
3) 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.
4) 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.
5) 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.
6) Write a custom login page to automatically login in users based in their Windows identity. See figure 6 below.
7) Write the custom membership provider class. See figure 7 below.
8) Write the custom role provider class. See figure 8 below.
9) 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 of application for windows purposes.........-->
<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 mechanism is "Forms".........-->
<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.
<!-- .......... Everything requires authorization.........-->
<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)
{
/*
The purpose of the code below is to
1) Get the Windows network login ID of the user requesting this page.
2) If not successful, this page will render in their browser, telling them they are an
unknown user.
3) If successful, we strip off the domain portion of the login ID, leaving just lastname and
initial(s).
4) We then read the user security (user) table for this user Id, attempting to retrieve the
password.
5) If we don't find a record, the page will render, same as if they did not have a Windows
login.
6) If we find a password, we call the ValidateUser method on the static class Membership.
This isreally an indirect reference through the static class to the custom
MembershipProvider class
that we wrote and "plugged in" to the security mechanism via the web.config file
(Membership section).
7) If we are not validated, we fall through and render the same text as if no Windows
login.
8) If we are successful, we store the full user name in the session and redirect to the
originally requested page which is normally Home.aspx.
*/
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))
{
nbsp;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)
{
// Add code here to read your custom security tables.
// Return true if user is valid, false if not.
}
// Override other methods as necessary..
}
|
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 instruction on how to implement a custom membership provider are available on MSDN at http://msdn2.microsoft.com/en-us/library/6tc47t75.aspx
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)
{
// Add code here to read your custom security tables.
// Return string array of role names.
}
// Override other methods as necessary..
}
|
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 instruction on how to implement a custom role provider are available on MSDN at http://msdn2.microsoft.com/en-us/library/tksy7hd7.aspx
|
| 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 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 string 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 work around 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 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 know 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 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.
|
| About the Author |
|
Joe Meirow is the application development manager and system architect for the Michigan Conference
of Teamsters Welfare Fund, located in Detroit, MI. Joe has been programming for 24 years in languages
ranging from assembler to Java to C#. He lives in Romeo, MI with his wife Ramona and daughters Monica
and Sarah.
Contact the author.
|
|
|
|