Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
51
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 6

Authenticating Classic ASP with ASP.NET

The next step is to build the functionality inside of the ASP.NET application to support forms authentication for classic ASP users. The general idea is that with both ASP pages and ASP.NET pages located in same virtual directory (and, thus, the same application in IIS6), you want unauthenticated users to be forced to authenticate using ASP.NET’s forms authentication mechanism.

After a user successfully logs in with forms authentication, the user should be redirected to the original requested page. This should occur regardless of whether the originally requested resource was an AS.NET page or a classic ASP page. On subsequent requests, again regardless of the type of requested resource, you want ASP.NET to transparently verify the validity of the forms authentication cookie and then pass the request along.

For starters, you need to configure the ASP.NET application with the basics necessary to enable forms authentication and enforce authenticated access:

<authentication mode=”Forms”/>

<authorization> <deny users=”?”/> </authorization>

With these settings, anonymous users will be redirected to the forms authentication login page. For now, just add a basic login page called Login.aspx to the sample application, and place a Login control onto the web page.

You can’t directly access default.asp at this point. Instead, because the wildcard mapping first routes the request to ASP.NET, and the ASP.NET configuration denies access to all anonymous users, you are redirected to the login page. In fact, anonymous requests never even make it to the logic inside of the

CustomHandler class. The UrlAuthorizationModule running during the AuthorizeRequest event in the HTTP pipeline detects that the user is anonymous and immediately forwards the call to EndRequest — in effect short-circuiting the request processing and bypassing the custom handler. The information about the original request to default.asp is still retained:

http://localhost/Chapter6/wildcardmappings/login.aspx?ReturnUrl=%2fChapter6%2fwildc ardmappings%2fdefault.asp

The next step is to add in a basic user store and authenticate credentials against that user store. I cover the new Membership feature in detail in Chapter 10, but for now the sample just uses the Membership feature with only a minor change to its default configuration. Because I happen to be running a local instance of SQL Server 2000, the connection string for all of the SQL-based providers (including Membership) needs to be changed:

<connectionStrings>

<remove name=”LocalSqlServer”/> <add name=”LocalSqlServer”

connectionString=”server=.;Integrated Security=true;database=aspnetdb”/> </connectionStrings>

272

Integrating ASP.NET Security with Classic ASP

All of the provider-based features that have SQL providers use the same connection string LocalSqlServer. For the sample application the default definition of LocalSqlServer is removed and is redefined to point at a local SQL Server instance running the aspnetdb database.

The login page for the application is Login.aspx, and again no special behavior is needed here. Just dropping a Login control onto the page is sufficient because the Login control automatically works with the Membership feature.

<%@ Page Language=”C#” AutoEventWireup=”true” CodeFile=”Login.aspx.cs” Inherits=”Login” %>

<html xmlns=”http://www.w3.org/1999/xhtml” > <head runat=”server”>

<title>Login Page</title> </head>

<body>

<form id=”form1” runat=”server”> <div>

<asp:Login ID=”Login1” runat=”server”> </asp:Login>

</div>

</form>

</body>

</html>

Now if you attempt to navigate to default.asp, you will be redirected to Login.aspx. Type in the some valid credentials (if you need to create some credentials first just use the ASP.NET Configuration tool from inside of Visual Studio), and log in. Assuming that the credentials are valid, you will be redirected back to default.asp, and you will have a valid forms authentication cookie for subsequent pages.

At this point in the sample, the custom handler isn’t really adding anything, though you rectify this shortly. The main thing to keep in mind is that with nothing more than a wildcard mapping, a slight tweak to a connection string, the forms authentication feature, and one login page you now have an ASP.NET application authenticating and logging users in prior to handing the users to classic ASP. Now that you know the steps involved you can whip up all this up in about five minutes flat! In fact, for many smaller ASP.NET-to-classic ASP integration problems, this may actually be all you need.

Will Cookieless Forms Authentication Work?

Cookieless forms authentication may not work as an authentication mechanism for classic ASP. For the heck of it, try adding the following to web.config.

<authentication mode=”Forms”> <forms cookieless=”UseUri” />

</authentication>

Initially, things will look like they are working, and you will successfully get redirected to default.asp. The resultant URL looks something like:

http://localhost/Chapter6/wildcardmappings/(F(vDq5hGYX8vci_pIoALoRV4_VoqUh37xIBfsak KtMk5khYLBT9W18ri5NgyR63wg3IgktUcYD95dsxHZuKPXgY4U5d85qgjrst2uLf2lgkM1))/default.asp

273

Chapter 6

The problem with this URL isn’t the fact that the cookieless forms authentication ticket is embedded in the URL. That actually won’t impact classic ASP because the ASP.NET ISAPI filter removes the ticket from the URL long before the request is forwarded to ASP.dll. Problems arise if your classic ASP code starts constructing redirects from inside of its code-base.

Chapter 5 explained that there were some restrictions on the way in which ASP.NET code could construct URLs and still retain the forms authentication ticket. ASP.NET provides the handy syntax to indicate an application-relative reference. However no such shorthand exists in classic ASP. You might have code in your classic ASP application that issues redirects with code like the following:

Response.Redirect(“/Chapter6/wildcardmappings/SomeOtherPage.aspx”);

This style of redirect will lose the forms authentication ticket that was embedded on the URL. Given the limited programming model available in classic ASP there isn’t an easy way to grab the ticket out of the URL and preserve it when you redirect. If your classic ASP application uses only relative redirects like the following then you will most likely be able to use cookieless forms authentication with a classic ASP application.

‘This type of redirect preserves the cookie-less ticket

Response.Redirect(“default2.asp”)

The same approach will work if you have any <a /> tags or other relative URL references in your classic ASP pages. From the browser’s standpoint relative URL references are always considered relative to the last path in the URL, which in the case of cookieless forms authentication means relative to the full URL including the cookieless ticket.

Passing Data to ASP from ASP.NET

Up to this point, you have seen the mechanics of getting forms authentication working with classic ASP. The next step is to come up with a way to pass the authenticated username over to the classic ASP application. There probably aren’t many ASP sites out there that require authentication but then throw away the authenticated username. The problem of getting the authenticated username over to the ASP application is just a specific example of the more general problem of passing data from ASP.NET over to a classic ASP application though.

This is where the custom HttpHandler comes in handy. Rather than having to cobble together some kind of redirection-based mechanism, you can use the HTTP headers for the request as a way to pass information along from ASP.NET into a classic ASP application. In fact for quite a few years, a variety of third-party authentication products have relied on manipulating HTTP headers as a platform-neutral way to pass information between different web applications.

In the case of a custom HttpHandler, you can change the HTTP headers for a request by using the protected ExecuteUrlHeaders property. You might think that you could just use the Context property to get to the Request.Headers property and then manipulate the resulting NameValueCollection. This will not work because Request.Headers is a read-only collection; its intended use in earlier versions of ASP.NET never included modifying the headers of a request. DefaultHttpHandler gets around this by storing a copy if the incoming HTTP headers in a separate NameValueCollection and making this collection available to developers via the ExecuteUrlHeaders property.

274

Integrating ASP.NET Security with Classic ASP

As an example, you can try adding an arbitrary header to the incoming request from inside of the custom handler.

public override string OverrideExecuteUrlPath()

{

this.ExecuteUrlHeaders.Add(“Some Custom Header”, “Some Custom Value”); return null;

}

Now, the custom HttpHandler inserts a new header value for the request. To verify that this custom HTTP header made it to the classic ASP page, you can add code to default.asp that dumps out the request headers.

<%

For Each value In Request.ServerVariables

if (value <> “ALL_HTTP”) AND (value <> “ALL_RAW”) then

%>

<b><%= value %></b> = <%= Request.ServerVariables(value) %> <br/><% End if

Next %>

The ASP code intentionally skips over the ALL_HTTP and ALL_RAW variables because these contain a concatenated dump of all of the headers in a rather unreadable form. If you open a browser and log in to default.asp, you get nicely formatted output showing all the request headers. At the end of the list, you will see the following:

HTTP_SOME CUSTOM HEADER = Some Custom Value

You can easily access custom HTTP header values from inside of classic ASP by just indexing into Request.ServerVariables. With this basic technique, you can pass information from ASP.NET 2.0 to classic ASP. As long as the information you need to pass can be serialized into a string in ASP.NET, and your classic ASP code can do something useful with that string value, you have a very easy way to pass information between the two environments. No need for kludgy redirects or expensive Web Service calls!

Although the samples in this chapter don’t need to move very much information around from ASP.NET to classic ASP, you might be wondering just how much data can you actually stuff into an HTTP header. As an experiment, you can try adding large strings into the header. The following code uses a 32KB string as the value for a custom HTTP header:

public override string OverrideExecuteUrlPath()

{

//gets called just before control is handed back to IIS6 //HttpContext c = this.Context;

this.ExecuteUrlHeaders.Add(“Some Custom Header”, “Some Custom Value”);

StringBuilder largeString = new StringBuilder(); largeString.Append(new String(char.Parse(“a”), 32768));

this.ExecuteUrlHeaders.Add(“A Very Large Header”, largeString.ToString());

return null;

}

275

Chapter 6

The custom header value “A Very Large Header” was passed to classic ASP without a problem, and the entire 32KB string showed up on default.asp. Part of the reason such enormous headers are allowed is that by the time ASP.NET is handing a request back to IIS6, the normal URL length and header size restrictions enforced by http.sys and ASP.NET have already occurred. Playing around with this a bit more, it turns out you can send as much as 65,535 bytes in an additional custom header (that is, 1 byte less than 64KB). Realistically though, for purposes of authentication and authorization, you aren’t going to need much more than a few kilobytes of space for username and role information.

Passing Username to ASP

Now that you have seen most of the work necessary to move information from ASP.NET over to classic ASP, the sample application should be extended to pass the authenticated username from ASP.NET forms authentication over to classic ASP. However, there is one very convenient piece of work that ASP.NET already performs on your behalf! A side effect of running the request through ASP.NET first is that the authenticated user information is automatically placed in the appropriate HTTP headers. For example, if you log in with the account testuser from ASP.NET, the header information that ASP.NET sets up for classic ASP already includes the following:

AUTH_USER = testuser

LOGON_USER = testuser

For classic ASP code that was already using either of these server variables to identify the user, integrating forms authentication and ASP couldn’t be easier.

Authorizing Classic ASP with ASP.NET

You have seen that forms authentication is already working with classic ASP application, in part because there is a URL authorization rule that denies access to anonymous users. In effect, you already have the basics of authorization working. The sample application though can be modified a bit more to include more extensive authorization rules.

For example, let’s say there is an administrative folder for the ASP application that should only grant access to users that are in the “Administrators” role. You can create a URL authorization rule that protects the ASP subdirectory.

<location path=”ASPAdminPages”> <system.web>

<authorization>

<allow roles=”Administrators”/>

<deny users=”*”/> </authorization>

</system.web>

</location>

Now, whenever an attempt is made to access a classic ASP page in the ASPAdminPages subdirectory, ASP.NET’s URL authorization will enforce this rule. Using the ASP.NET Configuration tool available from inside of Visual Studio you can enable the Role Manager feature, create a new role called “Administrators” and add a user to the new role. The only change that occurs in configuration is the addition of the <roleManager /> element (by default Role Manager is not enabled, hence the need to turn it on):

276

Integrating ASP.NET Security with Classic ASP

<roleManager enabled=”true” />

As with the Membership feature, the default Role Manager provider uses the LocalSqlServer connection string. Because this was changed earlier, Role Manager will automatically associate role information in the aspnetdb database with the user account information located in the same database.

At this point, if you try logging to a classic ASP page located within the ASPAdminPages directory, you get redirected to the login page for the application. If you log in with an account that you added to the “Administrators” role you can access pages in this subdirectory.

Once again you can see that once wildcard mappings are setup in IIS6, you just go about building authentication and authorization inside of ASP.NET as you normally would. The only difference is that the authorization rules also automatically protect access to the classic ASP pages. As with the authentication setup discussed earlier, even though there is a custom HTTP handler in the ASP.NET application, it still isn’t needed at this point. You could pull the custom HTTP handler, and everything shown so far with forms authentication and URL authorization would still function properly.

Passing User Roles to Classic ASP

By this point, you are probably wondering why there even is a custom HTTP handler in the ASP.NET application. Forms authentication and URL authorization seem to be working just fine; why is this handler sitting around in the application? Well, you finally made it to the point where the built-in magic of wildcard mappings runs out of steam. Even though authorizing classic ASP pages is useful, chances are that some of your ASP applications need the full role information for an authenticated user. Just protecting individual pages or entire subdirectories is not sufficient.

Solving this problem does require passing data from ASP.NET to classic ASP, and as a result you will need a custom HTTP handler to hand the role information of to your classic ASP pages. Because the sample application uses Role Manager, you can modify the custom handler in the application to pack the user’s roles into a custom header.

public override string OverrideExecuteUrlPath()

{

//gets called just before control is handed back to IIS6 HttpContext c = this.Context;

StringBuilder userRoles = new StringBuilder();

RolePrincipal rp = (RolePrincipal)c.User;

//Move the user roles into a semi-colon delimited string string rolesHeader;

if ( (rp != null) && (rp.GetRoles().Length > 0) )

{

foreach (string role in rp.GetRoles()) userRoles.Append(role + “;”);

rolesHeader = userRoles.ToString(0, userRoles.Length - 1);

}

else

rolesHeader = String.Empty;

this.ExecuteUrlHeaders.Add(“Roles”, rolesHeader); return null;

}

277

Chapter 6

First the custom HTTP handler gets a reference to the authenticated user on the context. Because the sample application enabled the Role Manager feature, the RolePrincipal is the object representation of an authenticated user that is attached to the current context automatically by the RoleManagerModule. You can then retrieve all the roles that a user belongs to from the RolePrincipal.GetRoles method.

When you run the sample application again, the role information can be seen in the “Roles” custom header. The original header name is prepended with HTTP_ by ASP which is why the following sample output has a header called HTTP_ROLES rather than just ROLES.

HTTP_ROLES = Administrators;Regular User;Valued Customer

The classic ASP pages can retrieve this role information in a more useful form by just cracking the header apart into an array.

<%

Dim arrRoles

arrRoles = split(Request.ServerVariables(“HTTP_ROLES”),”;”)

For Each role In arrRoles Response.Write(role) + “<br/>”

Next %>

This ASP page simply converts the string into an array, and then dumps the array out on the page. Assuming your classic ASP applications have some type of wrapper or common include function for retrieving roles and checking role access, you simply need to tweak that type of code to fetch the role information from the custom HTTP header instead.

Safely Passing Sensitive Data to Classic ASP

At this point, it almost looks like the authentication and authorization scenario is solved. Everything works, and you have a simple but very effective way for passing role information over to classic ASP. There is however one security problem with the previous code. Because the custom handler is manipulating a custom HTTP header, there are no special protections enforced for the header’s value. As a result, there isn’t anything that would prevent a malicious user from logging in, and then attempting to send a forged HTTP header called Roles that contained some roles that the user really didn’t belong to. This type of attack won’t work with HTTP headers such as LOGON_USER, because the value of these headers is automatically set in IIS and by ASP.NET. There isn’t any way that a malicious user could forge their username by sending fake headers to ASP.NET. However, with the theory that it is better to be safe than sorry, you can add extra protections into the custom HTTP handler that will make it impossible to create a forged header — regardless of how ASP.NET handles header merging. Just as forms authentication and other cookie-based features support digitally signing their payloads, you can also add a hash-based signature to your sensitive custom HTTP headers.

The sample defines a helper class that encapsulates the work involved in hashing string values as well as verifying hash values. The creation of a hash value for a custom HTTP header is performed from inside of the custom HTTP handler, while verification of the hashed header occurs inside of the classic ASP code. The need to access the same logic in both places means that the hash helper class also needs to be exposed via COM so that classic ASP can call into it.

278

Integrating ASP.NET Security with Classic ASP

Start by just defining the hash helper class and its static constructor:

namespace HashLibrary

{

public class Helper

{

private static string hashKey =

“a 128 character random key goes here”;

private static byte[] bKey;

static Helper()

{

//Cache the byte representation of the signing key bKey = ConvertStringKeyToByteArray(hashKey);

}

//snip...

}

}

Because the intent of this helper class is for it to create and verify hashes, some common key material must be shared across all applications that perform these operations. For a production application, you would use configurable keys, along the lines of <machineKey />, because this allows for flexible definition of keys and makes it easier to rotate keys. For simplicity though, the sample application hard-codes a 128character (that is, a 64-byte) key. You can easily generate one using the GenKeys sample code that was covered in Chapter 5. Needless to say, in a secure application you should never store key material inside code. For our purposes though, building a custom configuration section or dragging protected configuration into the mix at this point will simply clutter up the sample.

The hash functions inside the .NET Framework use byte arrays, so the string hash key needs to be converted. Because the private static variable holds the hash key as a string, it performs a one-time conversion of they key into a byte array inside of the static constructor. This one-time conversion eliminates the parsing overhead of having to convert the string hash key into a byte array every time the key is needed. The ConvertStringKeyToByteArray method is covered later in this chapter, although the purpose of the method is pretty clear from its name.

The helper class exposes a public static method that hashes a string value and returns the resulting hash as a string.

public static string HashStringValue(string valueToHash)

{

using (HMACSHA1 hms = new HMACSHA1(bKey))

{

return ConvertByteArrayToString( hms.ComputeHash(Encoding.Unicode.GetBytes(valueToHash))

);

}

}

279

Chapter 6

Because you don’t want an external user to be able to forge any of the custom HTTP header values, you need to use a hash algorithm that cannot be spoofed by other users. As with forms authentication, the sample code uses the HMACSHA1 algorithm because it relies on a secret key that will only be known by your application. Given a string value to hash, the HashStringValue method does the following:

1.Creates an instance of the HMACSHA1 algorithm, initializing it with the secret key.

2.Converts the string into a byte array because hash functions operate on byte arrays — not string.

3.Hashes the resulting byte array.

4.Converts the result back into a string using another helper method that will covered a little later.

Now that you have a convenient way to securely sign a string, you need a way to verify the signature.

public static bool ValidateHash(string value, string hash)

{

using (HMACSHA1 hms = new HMACSHA1(bKey))

{

if (HashStringValue(value) != hash)

return false;

else

return true;

}

}

The ValidateHash method is the companion to the HashStringValue method. In ValidateHash, given a piece of string data (the value parameter), and the digital signature for the data (the hash parameter), the method uses HMACSHA1 to generate a hash of the string data. Assuming that the piece of code that initially signed the string data, and thus generated the hash parameter, shares the same signing key, then hashing the value parameter should yield a hash value that matches the hash parameter.

Because the intent is for classic ASP pages to verify the hash values for custom HTTP headers, the logic inside of the ValidateHash method must also be made available through a COM interop.

#region COM support public Helper() { }

public bool ValidateHashCOM(string value, string hash)

{

return Helper.ValidateHash(value, hash);

}

#endregion

There are a few requirements to make a .NET Framework class visible via a COM wrapper. The class needs a default constructor because there is no concept of parameterized class construction in COM. Additionally, any methods exposed to COM must have signatures that are compatible with COM types. Because there isn’t the concept of static methods in COM, it was just easier to add a default constructor to the Helper class as well as a public instance method that simply wraps the public static ValidateHash method. From ASP.NET, you would use the static methods on the Helper class. From classic ASP and COM, you first instantiate an instance of the Helper class, and then call ValidateHashCOM on the instance.

280

Integrating ASP.NET Security with Classic ASP

The Helper class also has two methods for converting hex strings to and from byte arrays.

public static byte[] ConvertStringKeyToByteArray(string stringizedKeyValue)

{

byte[] keyBuffer = new byte[64];

if (stringizedKeyValue.Length > 128) throw new ArgumentException(

“This method is hardcoded to accept only a 128 character string”);

for (int i = 0; i < stringizedKeyValue.Length; i = i + 2)

{

//Convert the string key - every 2 characters represents 1 byte keyBuffer[i / 2] =

Byte.Parse( stringizedKeyValue.Substring(i, 2),

System.Globalization.NumberStyles.HexNumber

);

}

return keyBuffer;

}

The ConvertStringKeyToByteArray method is currently hard-coded to work only with 64-byte keys. Given a 128 character string (which is the hex string representation of a 64-byte value), the method iterates through the string extracting each set of two hex characters (0–9 and A–F). Each pair of hex characters is then converted into a byte value with a call to Byte.Parse. The net result is that a 128 character string is converted into a byte[64].

The reverse operation of converting a byte array into a string is shown here:

public static string ConvertByteArrayToString(byte[] value)

{

StringBuilder sb = new StringBuilder(128);

if (value.Length > 64)

throw new ArgumentException(

“This method is hardcoded to accept only a byte[64].”);

foreach (byte b in value)

{

sb.Append(b.ToString(“X2”));

}

return sb.ToString();

}

As with ConvertStringKeyToByteArray, the ConvertByteArrayToString method assumes 128character strings. Converting a byte array to a string is much easier because you can convert each byte value to a hex-string equivalent by using the string format of X2.

The only other work needed in the hash helper is to attribute the assembly so that the public Helper class is visible to COM. The assembly is also strongly named and will be deployed in the GAC.

281