|
|||||||||||||||||||||||
|
|||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
This article is in the Product Showcase section for our sponsors at The Code Project. These reviews are intended to provide you with information on products and services that we consider useful and of value to developers.
IntroductionMigrating 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:
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. ProcedureWith these requirements in mind, we developed the following approach, and it has worked quite well:
<!-- .......... 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 <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 <!-- .......... Everything requires authorization.........-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
Figure 5 - The 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))
{
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 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)
{
// 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 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.configIron Speed Designer stores database connection information in connection string format, but stores the key name and value in the 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 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 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. ConclusionIn 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.
|
||||||||||||||||||||||