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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 5

FormsAuthenticationTicket ft = ((FormsIdentity)c.User.Identity).Ticket;

Guid g = new Guid(ft.UserData);

MembershipUser loginUser = Membership.GetUser(ft.Name); string currentSessionString =

loginUser.Comment.Split(“|”.ToCharArray())[1]; Guid currentSession =

new Guid(currentSessionString.Split(“;”.ToCharArray())[1]);

//If the session in the cookie does not match the current session as // stored in the Membership database, then terminate this request if (g != currentSession)

{

FormsAuthentication.SignOut();

FormsAuthentication.RedirectToLoginPage();

}

}

}

}

The custom module hooks the PostAuthenticateRequest event so that it can inspect the authenticated credentials after the FormsAuthenticationModule has run. If the current request doesn’t have an authenticated user, the module exits. On the other hand, if there is an authenticated user, the module gets a reference to the FormsAuthenticationTicket and extracts the Guid login session identifier. The login information for the authenticated user is also retrieved from the Membership database.

The module is only concerned with checking the validity of the session identifier so that it doesn’t bother retrieving the expiration date from the MembershipUser instance because the FormsAuthenticationModule will already have made this check. The module does check the session identifier in the ticket against the session identifier stored in the database. If they match, the request is allowed to proceed. However, if the two identifiers do not match, this is indication that the current request is not associated with an active and valid login session. In this case, the module calls FormsAuthentication.SignOut, which has the effect of issuing a cookie that will clear the forms authentication cookie in the browser. Then the module redirects the current request to the login page for the application.

Because all of this logic is encapsulated in an HttpModule, the module needs to be registered in each application that wants to make use of its services. In terms of code deployment, for the sample application the code is in the App_Code directory; although again you can instead choose to author it in a separate assembly deployed in the bin or the GAC. Depending on how the module is deployed, you will need to add more information to the type attribute.

<httpModules>

<add name=”FormsAuthSessionEnforcement” type=”FormsAuthSessionEnforcement”/>

</httpModules>

252

Forms Authentication

Note that the sample code shown here only includes checks that make sense in the case of absolute ticket expirations. The custom module and login page do not handle the case where sliding expirations are enabled. You would need extra logic to periodically update the expiration data in the Membership database whenever the FormsAuthenticationModule renewed the ticket. As a result, the configuration for the sample application only allows absolute expirations.

<forms slidingExpiration=”false” />

When the module exits one of two outcomes has occurred: either the login session is valid and the request continues, or the session is invalid and the user is prompted to log in again. Assuming that the user is prompted for a login, this brings us full circle back to the login page. As shown earlier, there is a check box on the login page that allows a user to clear active login sessions. The setting of this check box, as well as the logic to prevent duplicate logins, is in the LogginIn event of the Login control.

protected void Login1_LoggingIn(object sender, LoginCancelEventArgs e)

{

if (loginUser == null)

loginUser = Membership.GetUser(Login1.UserName);

//See if the user indicates that they want an existing login session //to be forcibly terminated

CheckBox cb = (CheckBox)Login1.FindControl(“ForceLogout”); if (cb.Checked)

{

loginUser.Comment = String.Empty; Membership.UpdateUser(loginUser); return;

}

//Only need to check if the user instance already has login information //stored in the Comment field.

if ((!String.IsNullOrEmpty(loginUser.Comment)) && loginUser.Comment.Contains(“LoginExpiration”))

{

string currentExpirationString = loginUser.Comment.Split(“|”.ToCharArray())[0];

DateTime currentExpiration = DateTime.Parse((currentExpirationString.Split(“;”.ToCharArray()))[1]);

//The user was logged in at some point previously and the login is //still valid

if (DateTime.Now <= currentExpiration)

{

e.Cancel = true;

Literal tx = (Literal)Login1.FindControl(“FailureText”); tx.Text = “You are already logged in.”;

}

}

}

Duplicate login checks always require a MembershipUser to be handy, so the event first ensures that an instance is available. Because the LoggingIn event is always fired by the Login control before the LoggedIn event, the check that is made in the LoggedIn event will always find a MembershipUser instance already available for use.

253

Chapter 5

If the check box is selected (that is, the website user indicated that they want any active login session to be invalidated), the session information inside of the MembershipUser instance is cleared and the information is saved back to the Membership database. In essence, a setting of String.Empty in the MembershipUser.Comment field is an indication that the user is not logged in. One side note: to actually place the check box on the Login control required converting the control into a template. Template editing mode for the control allows you to add arbitrary controls to the layout. However, there is not a convenient strongly typed reference to any controls that you add — hence the need for calling FindControl to get a reference to the check box.

If there is login information contained in the Comment property, then the expiration date is extracted. From this, you can see that there are two different points in the application where expiration date and session identifiers are checked. The login session identifier is checked after the user is logged in. The expiration date is checked before the user is logged in. If the expiration date from the MembershipUser instance indicates that there is still a valid login session (that is, there is a session that will expire sometime in the future), then the remainder of the processing the Login control is halted by setting the Cancel property on the event arguments to true. A reference to the Literal control that displays error text is found, and appropriate error information is displayed to the user.

Each time a user logs in there are a few possible decision trees that will occur on the Login page:

1.The user is logging in for the very first time to the application. As a result all of the checks in the LoggingIn event are bypassed, and a login occurs.

2.The user is logging in after a previous login session already expired. In this case, the expiration date check in the LoggingIn event detects this, and the user is allowed to log in.

3.The user is logging in, but there is already a valid login session as indicated by the expiration date information within the Comment field. In this case, the login is not allowed to proceed and an error is returned.

4.The user is logging in and explicitly states that any previous session should be invalidated. This is similar to the first point with some extra work performed to clear the Comment field prior to allowing the login to proceed.

You can try all of this out by stepping through the process of logging in multiple times:

1.If you don’t already have a user, you can quickly create one by using the ASP.NET Configuration tool inside of Visual Studio (Website ASP.NET Configuration Tool).

2.Log in with a user to the sample site. If you look in the database, you will see login information inside of the Comment column of the aspnet_Membership database table. The data looks like:

LoginExpiration;5/22/2005 12:52:51 PM|LoginSessionID;71fa38d5-97f8-4c62- 8bbb-bac4ab2f352b.

3.Open up a second browser window, and type in the address of a secured page in the application. This will require you to log in again.

4.Note that when you attempt to log in in the second browser instance, the login fails because of the checks being made in the LoggingIn event on the login page.

5.Now attempt to login but make sure to click the check box to invalidate other login sessions. You will be able to log in at this point successfully. If you check the Comment column in the database, you will see updated information there.

254

Forms Authentication

6.Flip back to the first browser window and attempt to continue navigating around the site. You will instead get redirected back to the login page because of the login session ID check being made by the custom HttpModule. The module detects the login session in the first browser is no longer the active login session.

Enforcing a Logout

An issue that is related to the single login scenario is the potential for a user to reenter the site as a logged-in user after he or she has already logged out. If this sounds a bit strange, the following sequence of events can lead to this:

1.The user logs in and gets back a valid forms authentication ticket.

2.At some point in the future, the authentication ticket is hijacked or exposed.

3.The user logs out, thus clearing the forms authentication cookie his or her browser.

4.The malicious individual from step 2 replays the ticket back to the site. Assuming that the expiration date in the ticket is still valid, the malicious user can now run as an authenticated user.

In reality, the possibility of step 2 is open to quite a bite of debate. If you run your entire site under SSL (or at the very least set requireSSL to true in configuration), then hijacking a forms authentication from a network trace is not possible. Prior to ASP.NET 2.0 though, it was still possible to use some type of cross-site scripting attack to hijack a cookie using client-side browser code. However, in ASP.Net 2.0 the HttpOnly property of forms authentication cookies is set to true, so this attack vector is quite a bit harder to accomplish (though as noted earlier is may be possible to use the TRACE/TRACK command, which if supported on the web server still allow access to the cookie).

Furthermore, there isn’t anything in the steps listed earlier that would prevent this type of replay attack from occurring with a technically savvy user that sits down at a coworker’s machine and attempts to physically copy a cookie and email it back to himself (though even this attack would be partially mitigated by using only session based cookies). Anyway, the point here is that for high-security sites you don’t want to allow theoretical vulnerabilities, especially if there are reasonable steps that you can take to prevent the problem in the first place.

Because you have already seen the solution for preventing multiple logins, it is pretty easy to extend it one step further. A value of String.Empty in the MembershipUser.Comment field is already treated as an indicator that there is no active login session. If you add a LoginStatus control to the pages in your site, you can hook the LoggingOut event and perform some extra cleanup.

protected void LoginStatus1_LoggingOut(object sender, LoginCancelEventArgs e)

{

//Clear the information in Membership that tracks the //the current login session.

MembershipUser mu = Membership.GetUser(); mu.Comment = String.Empty; Membership.UpdateUser(mu);

}

Now whenever a website user explicitly logs out of a site, the login information for that user is deleted from the user record in the Membership database. With this change, there is one extra modification needed in the custom HttpModule as well.

255

Chapter 5

private void OnPostAuthenticate(Object sender, EventArgs e)

{

HttpApplication a = (HttpApplication)sender; HttpContext c = a.Context;

//If the user was authenticated with Forms Authentication //Then check the session ID.

if (c.User.Identity.IsAuthenticated == true)

{

FormsAuthenticationTicket ft = ((FormsIdentity)c.User.Identity).Ticket;

Guid g = new Guid(ft.UserData);

MembershipUser loginUser = Membership.GetUser(ft.Name);

Guid currentSession;

//If there isn’t any session information in Membership at this point //then it is likely the user logged out, and an old cookie is //being replayed.

if (!String.IsNullOrEmpty(loginUser.Comment))

{

string currentSessionString = loginUser.Comment.Split(“|”.ToCharArray())[1];

currentSession =

new Guid(currentSessionString.Split(“;”.ToCharArray())[1]);

}

else

currentSession = Guid.Empty;

//If the session in the cookie does not match the current session as // stored in the Membership database, then terminate this request if (g != currentSession)

{

FormsAuthentication.SignOut();

FormsAuthentication.RedirectToLoginPage();

}

}

The bolded section shows the changes to the module. Instead of just assuming that there will always be a value in the Comment property for the authenticated user, the module instead checks to see if the Comment property has any valid information in it. If there is no information in the Comment property, then the comparison between the session identifier in the forms authentication ticket and the value Guid.Empty always fails. If a malicious user attempts to replay an otherwise valid forms authentication cookie, and the true user logged out of the application, then the replayed ticket will never be accepted.

Looking at this code, you can see why for very secure sites, sliding expirations should never be used. Although you now have sample code that keeps track of the logged-in versus logged-out status of a user, there really isn’t much you can do to force a user to actually log out. How many of us just close down the browser when we are done with a site? In cases like this, the only remaining protection is for the forms authentication ticket to eventually expire. At least with absolute expirations the window of opportunity for a successful replay attack can be substantially narrowed. With sliding expirations, as long as a valid ticket is replayed to the site, the ticket will continue to work and will be periodically updated as well.

256

Forms Authentication

Summar y

Out of the box, forms authentication in ASP.NET 2.0 adds new protections by including the HttpOnly attribute on all forms authentication cookies. Used in conjunction with encryption and signing of the forms authentication ticket, the requireSSL attribute and absolute ticket expirations, you can quickly restrict the ability of malicious users to gain access to a forms authentication cookie.

ASP.NET 2.0 also introduces a cookieless mode of operation, whereby the forms authentication ticket is embedded in the URL. This makes it much easier for developers to author sites that work with mobile browsers as well as standard desktop browsers. In the interests of security though, developers should avoid cookieless forms authentication tickets for sites that require high degrees of security — it is simply too easy to “leak” or expose a cookieless forms authentication ticket to someone other than the original user.

Although forms authentication seems pretty simple, with a bit of custom code, you can actually solve some rather complex authentication problems. The new ability in ASP.NET 2.0 to pass forms authentication tickets across applications makes it possible to solve some single sign-on issues that previously required complex third-party SSO applications. Of course, there is also a limit to how far you can stretch the new cross application capabilities of forms authentication — for many developers commercial SSO solutions will still make sense.

The combination of forms authentication and Membership finally gives developers the basic plumbing needed to solve the single-logon problem. Although neither feature includes support for enforcing single-logons, both features are sufficiently extensible that with a reasonable amount of custom code you can prevent users from performing multiple logons. You can also provide protection so that when a user explicitly signs out, cookie replay attacks with a forms authentication cookie are not allowed.

257

Integrating ASP.NET Security

with Classic ASP

All of the great security features in ASP.NET don’t really help you when you look at your older classic ASP applications. Although forms authentication and URL authorization have been around since ASP.NET 1.0 days, these features haven’t been of any use in the ASP world. With the introduction of the Membership and Role Manager features in ASP.NET 2.0, you have even more authentication and authorization functionality built into ASP.NET. But again, it seems like that functionality is orphaned over in the ASP.NET world and never to made it over to the world of classic ASP.

Why attempt to bring the ASP.NET and classic ASP worlds together? In terms of sheer volume of code written, the majority of web applications out there are still running on classic ASP. Even if you surf around Microsoft’s own sites such as the MSDN online library and various links and subsites of www.microsoft.com, you still encounter a lot of classic ASP pages.

In ASP.NET 2.0 a number of small changes were made in some admittedly esoteric aspects of the runtime to make it possible to more tightly integrate ASP.NET and classic ASP. These changes also rely on modifications made earlier to IIS 6 around handling for ISAPI extensions. Both of these changes taken together make it possible to wrap classic ASP sites inside of ASP.NET

This chapter covers the following topics:

ISAPI extension mapping behavior in IIS 5

Wildcard mappings in IIS 6 and how they work

The DefaultHttpHandler in ASP.NET 2.0

Using the DefaultHttpHandler with ASP.NET and classic ASP

Authenticating classic ASP using ASP.NET

Adding roles from Role Manager for use in classic ASP

Chapter 6

IIS5 ISAPI Extension Behavior

Before ASP.NET there was IIS 5, and it was good. You could write classic ASP applications that incorporated their own authentication and authorization behavior. And you could add other external resources like images, stylesheets, and so on and reference them from your classic ASP applications. However, sometimes you wanted to perform some preliminary work prior to passing a request on to ASP. Probably the most frequently asked for (and unfortunately will still be asked for even with ASP.NET 2.0) capability was URL rewriting.

However, in IIS5 the only way to accomplish something like this was by writing an ISAPI filter — a rather daunting prospect for most us (and believe me I include myself in this classification). The underlying reason for this restriction is in that in IIS5 the core runtime is only extensible through ISAPI filters and extensions; that was the extensibility mechanism at the time.

Of course, one nice side effect in IIS5 was that the authentication model for classic ASP was the IIS authentication model. There was no artificial bifurcation between IIS authentication modes and some other ASP-like authentication mode. This meant that after you had things configured in IIS, your ASP security just worked with IIS’s implementation of integrated security. Furthermore, when an ASP application relied on just plain HTML pages, image files, CSS files, and the like, there wasn’t any need for special security configuration work to get these to work. ASP, IIS, and static files lived together peacefully.

Then along came ASP.NET 1.0 and 1.1 running on top of IIS 5 — and the security story became a little weird. ASP.NET security was in its own world, though as you saw back in Chapter 1 a variety of mechanisms were developed to hop security information from the IIS world into the ASP.NET world. However, one scenario that was definitely lost was that ASP.NET pages and classic ASP pages were oblivious of one another.

In ASP.NET, you finally had a way to modify parameters of an incoming request prior to having a page run. But if you were thinking you could shoehorn classic ASP into ASP.NET to take advantage of the HttpModule extensibility in ASP.NET, you were sorely disappointed. The core technical reason for this is that in IIS 5, when a request is mapped to an ISAPI extension, that is the end of the road for that request. After the request is handed off to a specific ISAPI extension, the mapped extension owns the request for the rest of its lifetime.

There was no concept in IIS5 of being able to route a request to one extension (aspnet_isapi.dll as discussed in Chapter 1), and then somehow reroute the request to another extension, for example asp.dll, which is responsible for .asp and .asa files. Of course, you could get a little enterprising and implement some redirection-based mechanisms that hopped information back and forth between classic ASP and ASP.NET, but those solutions always end up being a bit awkward. Any customer on a slow Internet link is also aware of the overhead involved with all these redirects, which usually makes any such solution chancy at best for those still living in a 56K world.

There was another problem with the ISAPI extension handling in IIS5 when using ASP.NET, and that was in the area of static file handling. As you saw in Chapter 2 in the section on blocking access to non-ASP.NET file types, most common static file extensions are already mapped to ISAPI extensions or to the core IIS runtime itself. As a result, if you wrote an ASP.NET application that needed to protect access to XML or .htm files, you had to explicitly map each of these file extensions to the ASP.NET ISAPI extension. If you didn’t carry out this step, IIS5 would happily serve the files directly without any authentication or authorization by ASP.NET. Of course, if your HTML or XML files happened to include sensitive data this wasn’t exactly the desired outcome.

260

Integrating ASP.NET Security with Classic ASP

What was especially aggravating with IIS5 was that if you had more than one or two static file extensions to be protected by ASP.NET, you had to go through a fair amount of manual configuration on each of your web servers to ensure the correct association of static file types to ASP.NET. And of course if you wanted a mixture of authentication and authorization policies for these files (for example, maybe some images were viewable by everyone, but others need to be secured) you had two choices:

Have all requests for the static files flow through ASP.NET — in which case you would encounter slower performance when serving the static files for anonymous users.

Separate the files that were accessible to anonymous users into one directory structure outside of ASP.NET, so they could take advantage of the faster file-serving performance afforded by IIS 5.

Both of these options had their shortcomings: You could trade off performance for centralized management of authentication, or you could get optimal performance but with the overhead of keeping two different directory structures for anonymous and authenticated users.

IIS6 Wildcard Mappings

IIS6 introduced the concept of wildcard mappings. Wildcard mappings are a way to tell IIS6 that every incoming request, regardless of file type, should be routed to one or more ISAPI extensions. Since these extensions are configured in IIS6 to handle any incoming request the term “wildcard” is used to indicate that request handling is independent of a specific file type. Not only can you configure a single ISAPI extension with wildcard mappings, but you can also configure multiple ISAPI extensions to act as a chain of wildcard mappings. IIS6 will walk through the list of configured mappings in sequence, passing control of the request to each extension in turn.

After the wildcard mapped extensions have completed their processing, IIS6 passes control of the request to the extension or internal runtime handling appropriate for the file type. The IIS6 ISAPI API also included additional functionality for extension authors that know their extensions will be used as part of a wildcard mapping. In the case of ASP.NET 2.0, the DefaultHttpHandler class (covered in the “DefaultHttpHanlder” section this chapter) includes extra logic that allows ASP.NET to gain control of a request for non-ASP.NET resources both before and after the default processing for that request occurs. This enables you to integrate ASP.NET 2.0 in a way that it can perform both preprocessing and postprocessing of a classic ASP request.

Configuring a Wildcard Mapping

To keep things simple initially, let’s take a simple ASP page and a simple ASP.NET application and configure the two to work together using an IIS6 wildcard mapping. After creating the basic folder structure, and marking the folder as an application in IIS6, the next step is to add a wildcard mapping so that all requests for resources will first flow through ASP.NET.

After you right-click on the application in the IIS6 MMC and select Properties, the Properties dialog box shown in Figure 6-1 has a Configuration button that leads to another dialog box.

261