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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 5

The slidingExpiration and timeout attributes define the expiration behavior for the master forms authentication ticket. Because the master ticket is also cloned and used as the source for tickets sent to other participating applications, this means these attributes also define the expiration behavior for all other applications. In the case above, the central login application is using the standard timeout of 30 minutes, and it is allowing sliding expirations. Remember, though, that slidingExpiration is always set to false in all of the participating applications. This point will be expanded on in a little bit when I cover the login page.

The login page in the central login application normally would have the user interface for collecting credentials and validating them. However, because this is just a sample that focuses on the mechanics of passing tickets around, the actual “login” on the page is pretty basic and uses a fixed credential:

protected void Button1_Click(object sender, EventArgs e)

{

FormsAuthentication.SetAuthCookie(“testuser”, false);

string redirectUrl = Request.QueryString[“CustomReturnUrl”];

string cookiePath = Request.QueryString[“CustomCookiePath”];

Response.Redirect(“Login.aspx?CustomReturnUrl=” + redirectUrl + “&CustomCookiePath=” + cookiePath, true);

}

Rather than calling FormsAuthentication.RedirectFromLoginPage, the button click handler for login calls SetAuthCookie. Calling SetAuthCookie ensures that the master forms authentication cookie is set in the Response, but it also allows the login page to do other work and then programmatically issue a redirect.

Because the CustomReturnUrl and CustomCookiePath attributes are still needed, the click event handler simply moves the values from the inbound Request query-string to the query-string variables on the redirect. The important thing to note about the click event handler is that it will only be called when an interactive login is required. The very first time website users enter any participating site, they will end up with the interactive login and their response will flow the click event handler. However, as the following code shows, the login page also supports noninteractive login:

protected void Page_Load(object sender, EventArgs e)

{

//If the user is already authenticated, then punt them back

//to the original application, but place a new forms authentication //ticket on the query string.

if (User.Identity.IsAuthenticated == true)

{

//This information comes from the forms authentication cookie for the //central login site.

FormsIdentity fi = (FormsIdentity)User.Identity;

FormsAuthenticationTicket originalTicket = fi.Ticket;

//For sliding expirations, ensure the ticket is periodically refreshed. DateTime expirationDate;

if (FormsAuthentication.SlidingExpiration == true)

{

TimeSpan timeout = originalTicket.Expiration.Subtract(originalTicket.IssueDate);

242

Forms Authentication

expirationDate =

originalTicket.IssueDate.Add(new TimeSpan(timeout.Ticks / 2)); expirationDate.AddMinutes(1);

}

else

expirationDate = originalTicket.Expiration;

FormsAuthenticationTicket ft = new FormsAuthenticationTicket

(originalTicket.Version,

originalTicket.Name,

originalTicket.IssueDate,

expirationDate,

originalTicket.IsPersistent,

originalTicket.UserData,

Request.QueryString[“CustomCookiePath”]

);

string redirectUrl = Request.QueryString[“CustomReturnUrl”];

Response.Redirect( redirectUrl + “?” +

FormsAuthentication.FormsCookieName + “=” + FormsAuthentication.Encrypt(ft));

}

}

Actually, what happens when a website used first needs to login against the central login application is that the Load event handler ran. However, because this event handler falls through for unauthenticated users, the very first time a user needs to log in he or she instead ends up with the login page being rendered and can perform an interactive login.

The noninteractive login occurs on most subsequent requests. For example, the button click handler for the login page redirects back to the same page. When the redirect comes back to the login page, there is now a master forms authentication ticket sent along with the request (from the SetAuthCookie call in the button click handler). As a result, when the Load event runs again, it sees that the user is authenticated, and so no interactive UI is even rendered.

The Load event first gets a reference to the master forms authentication ticket because it needs most of the information in that ticket to create a forms authentication ticket for the participating site. The Load event creates a new forms authentication ticket and carries over almost all of the settings from the master forms authentication ticket. For example, this means a participating site gets the exact same issue date and expiration date as the master forms authentication ticket. If you build a similar solution, you could choose to actually store DateTime.Now for the IssueDate of the new ticket. The main point, though, is that the expiration date for tickets sent to participating sites is based on the expiration date for the login against the central login application.

If you use absolute ticket expiration in the central login application, the behavior when tickets timeout in participating applications is pretty clear. When a forms authentication ticket times out in a participating application, the request is redirected through the local login page, which ends up requesting the central login page. However, because all tickets use the same timeout values, the master forms authentication ticket has also timed out. As a result, the redirect to the central login application falls through the Load

243

Chapter 5

event (the user is no longer considered authenticated), and instead the interactive login is shown. When the interactive login completes, a new master forms authentication ticket is issued, and the second execution of the login page results in a redirect with a new ticket and a new expiration date back to the participating application.

On the other hand, if you use sliding expirations in the central login application, the reauthentication should be transparent to the website user. The ticket for the participating application is issued with a modified expiration date. Instead of using the same expiration date as the master forms authentication ticket, the time to live for the ticket is set to half the TTL for the master forms authentication ticket, plus one extra minute. Because you know that forms authentication automatically reissues tickets when 50% or more of the remaining time to live has passed for a ticket, the idea is to create a ticket for the participating applications that will timeout in a similar manner. The extra one minute is added to account for clock-skew between the central login application and participating applications.

What happens now is that in the participating applications with absolute expirations, the forms authentication ticket eventually times out at (IssueDate + 50% of the central login application’s timeout + 1 minute). This results in a redirect back to the central login page. However, because (ExpirationDate — 50% of the central login application’s timeout — 1 minute) of time remains on the master forms authentication ticket, the master ticket is still considered valid. On the other hand though, because the master forms authentication ticket has less than 50% of its remaining lifetime left, the FormsAuthenticationModule in the central login application will automatically renew the master forms authentication ticket — which results in a new

IssueDate and a new ExpirationDate.

Because the renewal occurs in the HTTP pipeline before the login page ever runs, by the time the Load event executes, a new master forms authentication ticket is available. As a result, the ticket that is created for the participating application contains a new IssueDate and an ExpirationDate roughly equal to (DateTime.Now + 50% of the central login application’s timeout + 1 minute). When this ticket is sent back to the participating application, it results in a valid forms authentication ticket, and so the website user is returned to the originally requested page. Although a few redirects occurred underneath the hood, there was no interactive login required to renew the cookie.

Another property in the new forms authentication ticket that differs is the CookiePath. Rather than cloning over the cookie path from the forms authentication ticket, the value from the CustomCookiePath query-string variable is used instead. This is how the central login application ensures that the ticket sent back to the participating application has the correct path information. The FormsAuthenticationModule in the participating application will use the CookiePath value from this ticket when it constructs and issues the forms authentication cookie.

The CustomReturnUrl query-string variable is used to build the redirect URL. Because this value includes the full qualified path back to a page in the participating application, the redirect issued by the central login page can cross servers and domains. You can see the chain that leads up to this point as well:

1.Participating application creates the fully qualified return URL

2.Central login application replays fully qualified return URL when it redirects to itself

3.Central login application uses replayed fully qualified return URL when it redirects back to the participating application

The actual redirect includes the query-string variable and value with the forms authentication ticket. It uses the exact same code as you saw earlier when cross-application redirects were first introduced.

244

Forms Authentication

The Final Leg of the SSO Login

At this point, a redirect has been issued back to the participating application, to the specific page that the website user was originally trying to access. The user is able to navigate around the participating application because now there is a valid forms authentication cookie. If the cookie eventually times out, the behavior described earlier around ExpirationDate takes effect, and a new ticket is issued.

If the website user surfs over to another participating application, there is of course no forms authentication cookie for this third application. However, the exact same logic applies. In the third application:

1.A redirect to the local login page occurs.

2.The local login page redirects to the central login application.

3.Because the master forms authentication ticket exists, the central login application transparently creates a new ticket and sends it back to the participating application.

4.The participating application converts the ticket in the query-string into a valid forms authentication cookie, and the originally requested page runs.

Examples of Using the SSO-Lite Solution

Using a sample participating application called AppAUsingCentralLogin, the initial attempt to fetch default.aspx results in a redirect to the interactive login page in the central login application. The URL at this point looks like (bolded areas inserted for clarity):

http://demotest/Chapter5/CentralLogin/Login.aspx?CustomReturnUrl=http%3a%2f%2fdemot est.corsair.com%2fChapter5%2fAppAUsingCentralLogin%2fDefault.aspx&CustomCookiePath= %2fChapter5%2fAppAUsingCentralLogin

You can see that the URL is pointed at the central login page. The CustomReturnUrl query-string variable contains the URL-encoded representation of a test server as well as the full path to default.aspx. The CustomCookiePath query-string variable contains the path information that was set in the <forms /> configuration element of the participating application /Chapter5/AppAUsingCentralLogin.

After successfully logging in, you are redirected back to the originally requested URL. The URL in the address bar at this point looks like:

http://demotest.corsair.com/Chapter5/AppAUsingCentralLogin/Default.aspx?.ASPXAUTH=C

5338638F07C49516DA6B055BC12474D3266A0688F395C7BDAF29C2254478922507DC996699848AF4E8A

FA793521153C6A4C40FCC7EA602061706FC5DA67F42CDBFA07643349D12DB24020CCAF0F5FD4C618BD1

4BBF9A038116FDDEA9F39196C2AC8CA0CA2B570367D4B72A65C2E3D573EB619E1FF9BF9F648F43889BA

C00BBF51B1B361C2EAC02C

Because the SSO-lite solution relies on cross-application redirects, the very first page that is accessed after the redirect from the central login application includes the forms authentication ticket sitting in the query-string. If you navigate around into the site though, this query-string variable goes away:

http://demotest.corsair.com/Chapter5/AppAUsingCentralLogin/AnotherPage.aspx

If you now navigate over to a second participating application:

http://demotest.corsair.com/Chapter5/AppBUsingCentralLogin/Default.aspx

245

Chapter 5

There is a slight pause while the redirects occur, but you end up on default.aspx, with the address bar showing the following:

http://demotest.corsair.com/Chapter5/AppBUsingCentralLogin/Default.aspx?.ASPXAUTH=B

22EDE80C1D97F37E2512FCBA2AA0E1734208A6D3971D78E3CFFA8A28AF4D4C16624830AD0FD3BE1DD16

8452415323A226A34E2E86D2E8EE1A5635CDDB8BF47D66B0DB3D773DCFB3BF93A159F03F1D61530966B

2ED9D64AD408E1ED2FFF565862F2C256D9FC3EE5D136FC566B159953ADAF4A80DB632E37A934117F098

F8C2845D99AC2138FA3503

No prompt for login occurs though because the master forms authentication cookie has already been issued. As with the first participating application, the initial redirect from the central login application back to application B (in this case), results in the forms authentication ticket showing on the URL. When you navigate deeper into the site, this will go away.

Although I can’t show it here in a book, if you take the code for the central login application in Visual Studio and attach to w3wp.exe with the debugger you can see how tickets are renewed in the sliding expiration case with the following steps:

1.Set the timeout attribute in the central login application to three minutes or more.

2.Access one of the participating applications and go through the login process.

3.Attach the central login application with the debugger and set breakpoints in the Load event of the login page.

4.Wait for 2.5 minutes (50% of the central application’s timeout plus one minute). This is the timeout on the ticket sent to the participating application.

5.Access another page in the participating application. At this point, you will see that the breakpoints in the central login page are hit and a new forms authentication ticket is issued for the participating application. If you inspect the new IssueDate and ExpirationDate, you will see that they have all been updated with new values. Because the master forms authentication ticket was 2.5 minutes old when the redirect back to the central login application occurred, the FormsAuthenticationModule in the central login application automatically renewed the master ticket as well.

Final Notes on the SSO-Lite Solution

You have seen that with cross-application redirects in ASP.NET 2.0’s forms authentication that it is possible to sort of cobble together an SSO-like solution. However, now that I have shown how to accomplish it, there are a number of technical points that you still need to keep in mind.

The solution depends entirely on redirects between different servers and different domains. There may be the possibility of getting browser security warnings when running under SSL and a redirect occurs to a completely different application and DNS domain.

Because of the dependency on redirects, you need to be careful in how participating applications are structured as well as in the ticket timeouts. It is entirely possible that a user working on a form in an application posts data back to the server and then loses all of the information when a silent reauthentication with the central login site occurs.

In the case of sliding expirations, the sample depends on very specific behavior around the renewal of forms authentication tickets. Although this renewal behavior is documented, the trick with adding a one minute offset is fragile — both due to the potential for changes in the

246

Forms Authentication

underlying forms authentication behavior as well as the variability around clock skew between participating applications and the central login server. A more robust solution could involve a custom HttpModule installed on each participating site that would optionally renew the ticket based on information carried in the UserData property of the ticket.

You may want more control over how ticket timeouts are handled in general — both for the master forms authentication ticket and for the participating sites. For example, you may want configurable ticket timeouts that vary depending on which participating application is requesting a ticket.

There was no concept of federation or trust shown in the sample SSO solution. For an in-house IT shop, this probably would not be an issue because developers at least know of other development organizations sharing server farms and there is an implicit level of trust. However, in the case of disparate Internet facing sites run by different companies, trust is an incredibly important aspect of any SSO solution. Attempting to create an SSO solution on top of forms authentication for such a scenario probably isn’t realistic.

Last, the sample application allows any participating application to make use of it. With the prevalence of phishing attacks on the Internet these days, you would want to some additional security in an SSO-lite solution. At a minimum, you would want the central login application to only accept login attempts from URLs that are “trusted” by the central login application. This would prevent attacks where a malicious website poses as the login page to a legitimate site, and then through social engineering attacks (that is, unwary user clicking through a spam email) harvests a valid forms authentication ticket issued by the central login application. This specific scenario is why for more complex SSO scenarios you would want to use a commercial SSO product that incorporates the concept of trust — both trust between participating sites as well as trust between applications and the website that issues credentials.

Overall, I think these points highlight the fact that cross-application redirects can definitely be used for solving some of the simpler problems companies run into around single sign-on. However, if you find that your websites require more than just a basic capability to share tickets across servers and applications, you will probably need to either write more code to handle your requirements or go with a thirdparty SSO solution.

Enforcing Single Logons and Logouts

A question that comes up from time to time is the desire to ensure the following behavior when users login with forms authentication:

Users should be allowed to login once, and only once. If they attempt to login a second time in an application the login should be rejected.

If users explicitly log out, the fact that they logged out should in some way be remembered to prevent replaying previous authentication tickets.

Both of these design questions highlight the fact that forms authentication is a lightweight mechanism for enforcing authentication. Forms authentication as a feature does not have any back-end data store. As a result there isn’t an out-of-box solution that automatically keeps track of login sessions and subsequent logouts. However, with a little bit of coding it is possible to deal with both scenarios in ASP.NET 2.0.

247

Chapter 5

The solution outlined in this section relies on the Membership feature of ASP.NET 2.0. There is an extensive discussion of extending Membership in Chapters 10, 11, and 12 — however, because this chapter deals with forms authentication it makes more sense to show the Membership-based solution at this point rather than deferring it. Because Membership is designed to work hand-in-hand with forms authentication, it is a logical place to store “interesting” information about the logged-in or logged-out state of a user account. Of course, you could write your own database solution for the same purposes, or possibly even use the new Profile feature in ASP.NET 2.0 for similar purposes, but given that Membership is readily available and is part of the authentication stack in ASP.NET 2.0, it makes sense to leverage it.

Enforcing a Single Logon

For the first scenario of preventing duplicate login attempts, the fact that Membership stores its information in a database (or in AD and ADAM if you so choose) makes it very useful in web farms. Any information stored into the MembershipUser instance for a logged-on user will be available from any other web server in the farm. In the same vein, because Membership providers can be configured in multiple applications to point at the same database, it is also possible to use information in a MembershipUser instance across multiple applications.

The MembershipUser object doesn’t have many places for storing additional information. However the Comment property on MembershipUser is not used by ASP.NET, so it is a convenient place to store information without needing to write derived versions of MembershipUser as well as derived versions of

MembershipProvider(s).

Enforcing the concept of a single logon requires tracking two pieces of information associated with a successful logon:

The expiration time for the successful logon

Some type of identifier associated with the logon

Knowing when a successful logon expires is important because most website users probably never use explicit logout mechanisms. Instead, most users navigate through a site, perform whatever required work there is and then close the browser. In this case, if a user comes back to the site at a later point after the original logon session has expired, you don’t want to nag the user about preexisting logon sessions that have since expired. Instead, you want an authentication solution that recognizes the previous logon has expired and silently cleans up after the fact.

The second piece of information is important to keep track of because you need some concrete representation of the fact that a user logged in to the website. Just storing an expiration date is not sufficient. An expiration date indicates when an active logon session expires, but the date alone doesn’t give you enough information to correlate to the fact that someone logged in to a website. By tracking some type of session identifier, you can check on each inbound request whether the authentication data is for the active logon session or for some other logon session.

A logon session identifier also gives the website user the ability forcibly logout another active session. This scenario is important if, for example, a user logs in to your website on one machine and forgets about it. Then the user walks down the hallway to another machine and attempts to login again. With the logon session identifier, you have a way to allow the user to log on using other machines while ensuring that the previous logon session (or sessions) that are sitting idle on some other machine cannot be reused when the individual gets back to his or her desk.

248

Forms Authentication

So, just from this brief overview of the main problems involved with enforcing a single login you can see that there is a fair amount of tracking and enforcement necessary to get all this working. The good thing though is that it is possible to build this type of enforcement using the existing forms authentication and Membership features.

You will start out building the solution by looking at a sample login page. Since ASP.NET 2.0 conveniently includes the UI login controls, building the basic UI with logical events during the login process is a snap. Drop a login control onto a page, and then convert into a template. Converting it into a template allows you to add UI customizations as needed. In this case, you need to add a check box that allows an end user to forcibly logout other active logon sessions.

<!-- snip --> <tr>

<td colspan=”2”>

<asp:CheckBox ID=”ForceLogout” runat=”server”

Text=”Check here to invalidate other logon sessions.” />

</td>

</tr>

<!-- snip -->

So much for the UI aspect of the login control. Switching to the code-behind for the page, there are two events that you want to handle:

LoggingIn — This event gives you the opportunity to perform some checks before the Login control attempts to validate credentials using the Membership feature. It is a good place to check and see whether or not another active logon session is in progress.

LoggedIn — This event occurs after the Login control has successfully validated credentials. Because enforcing a single login requires some extra work on your part, this is the logical point to create a FormsAuthenticationTicket with extra information and issue it.

The LoggedIn event is where you store information inside of Membership that indicates the logon session ID as well the session expiration inside of the forms authentication ticket.

//snip..

protected MembershipUser loginUser;

protected void Login1_LoggedIn(object sender, EventArgs e)

{

if (loginUser == null)

loginUser = Membership.GetUser(Login1.UserName);

//represents the active login “session” Guid g = System.Guid.NewGuid();

HttpCookie c = Response.Cookies[FormsAuthentication.FormsCookieName]; FormsAuthenticationTicket ft = FormsAuthentication.Decrypt(c.Value);

//Generate a new ticket that includes the login session ID FormsAuthenticationTicket ftNew =

new FormsAuthenticationTicket( ft.Version,

ft.Name,

249

Chapter 5

ft.IssueDate,

ft.Expiration,

ft.IsPersistent,

g.ToString(),

ft.CookiePath);

//Store the expiration date and login session ID in Membership loginUser.Comment =

“LoginExpiration;” + ft.Expiration.ToString() + “|LoginSessionID;” + g.ToString();

Membership.UpdateUser(loginUser);

//Re-issue the updated forms authentication ticket Response.Cookies.Remove(FormsAuthentication.FormsCookieName);

//Basically clone the original cookie except for the payload HttpCookie newAuthCookie =

new HttpCookie( FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ftNew));

//Re-use the cookie settings from forms authentication newAuthCookie.HttpOnly = c.HttpOnly; newAuthCookie.Path = c.Path;

newAuthCookie.Secure = c.Secure; newAuthCookie.Domain = c.Domain; newAuthCookie.Expires = c.Expires;

//And set it back in the response Response.Cookies.Add(newAuthCookie);

}

After a successful login, the page first ensures there is a MembershipUser reference available for the user that is logging in. The GetUser(...) overload that accepts a username must be used because even though the user’s credentials have been successfully verified at this point, from a forms authentication viewpoint, the page is still running with an anonymous user on the current HttpContext. It won’t be until the next page request that the FormsAuthenticationModule has a cookie on the request that it can convert into a FormsIdentity.

Because the LoggedIn event won’t run unless other preliminary checks ensure that it is alright for the user to login, there aren’t any other validation checks in this event handler. To reach this event, the credentials will already have been verified as matching, and the other checks in the LoggingIn event (shown a little bit later) will also have been passed.

For this sample, a Guid was chosen as the representation of a login session — so the event handler creates a new Guid to represent a new instance of a login session. As you have seen in other sections, because the forms authentication APIs don’t expose timeout information, you need to get to it through a workaround. In this case, because the Login control has already called SetAuthCookie internally, there is a valid forms authentication cookie sitting in the Response. With this cookie, you can get the FormsAuthenticationTicket for the user that is logging in.

250

Forms Authentication

A new FormsAuthenticationTicket is created that is a clone of the already issued ticket, with one difference. The UserData information in the ticket is where the Guid login session identifier is stored. Note that because this sample application relies on the UserData property, enforcing a single logon in this manner will only work with clients that support cookies. The Expiration and the Guid for the ticket are also packaged up and stored in the MembershipUser instance for the user that is logging in. In more complex applications, you could create a custom class that represented this type of information, run the class through the XmlSerializer, and store the output in the Comment property. For simplicity though, the sample application stores the information with the following format:

LoginExpiration;expiration_date|LoginSessionID;the_Guid

Each piece of information is a name-value pair, with different name-value pairs delimited with the pipe character. Within a name-value pair, the two pieces of information are delimited by a semicolon. Once the Comment field has the new information, Membership.UpdateUser is called to store the changes back to the database.

The last piece of work during login is to replace the forms authentication cookie issued by the Login control with the FormsAuthenticationTicket that has the UserData in it. Again, rather than attempting to hard-code pieces of forms authentication configuration information into the application, the sample code simply reuses all of the settings from the Login control’s cookie to create a new cookie with all of the correct settings. The Login control’s original cookie is then removed from the Response, and the new cookie is added in its place.

At this point, when the login page completes, the user is successfully logged in with the session identifier flowing back and forth between the browser and the web server inside of the forms authentication ticket. There is also a persistent representation of the expiration time for the login as well as the session identifier stored in the Membership system. These pieces of information form the basis for checking the validity of a login on each and every request.

Because the FormsAuthenticationModule runs during the AuthenticateRequest event in the pipeline, it makes sense to perform additional validations after forms authentication has performed the basic work of determining whether or not there is a valid forms-authenticated user for the request. A custom HttpModule is used to enforce that the current request is associated with the current login session.

public class FormsAuthSessionEnforcement : IHttpModule

{

public FormsAuthSessionEnforcement(){} public void Dispose() {}

public void Init(HttpApplication context)

{

context.PostAuthenticateRequest += new EventHandler(OnPostAuthenticate);

}

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)

{

251