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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 5

http://localhost/Chapter5/cookiedAppB/default.aspx?.ASPXAUTH=23CB12E603239A53830866

D67D38DE6E8AAAA3647A05220FB278A5B6A3A0C0927FC498D3E6ED46AEBD7EF770AC3359CABE08EDC63

385D8C058B58D0C63782A27F948A8A8BFF5DFE9CE2C78463C68E1C0EB390B6C89CB594D21564EF94B28

66CA112AFE132F904FF87FF728B6DD3A48E6

Although it looks a bit strange, this is actually innocuous. After you start navigating around in the second application, the query-string variable will go away:

1.When the current page posts back to itself, the query-string variable will flow down to the application.

2.The FormsAuthenticationModule first looks for valid tickets in cookies and embedded in the URL. Because it finds a valid ticket in a cookie, it never makes it far enough to look at the querystring variable.

3.The current page runs.

4.Eventually you click on a link or trigger a redirect to some other page in the application. When this occurs the query-string is not sent along with the request, and as a result other pages in the application won’t have the ticket sitting in the address bar.

Because the point at which step 4 occurs is probably not deterministic (a website user may be able to enter into the application from any number of different pages), the query-string variable can end up in the address bar for any of your entry pages.

As with cookieless cross application redirection, if you happen to set requireSSL to true in your applications, the hop from one application to another will cause the FormsAuthenticationModule to check the secured state of the connection. If the module detects that the cross-application redirect occurred on a non-SSL connection, it will throw an HttpException, just as it would for the cookieless scenario.

Unlike the cookieless case though, you do have another option for hopping credentials from one application over to another. You can choose to post the forms authentication ticket from one application to another because you don’t need to worry about the extra redirect the FormsAuthenticationModule performs when embedding the ticket into the URL. To show this, create another page in first application:

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

<title>Untitled Page</title> </head>

<body>

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

<asp:TextBox ID=”txtSomeInfo” runat=”server”></asp:TextBox><br /> <br />

<asp:Button ID=”Button1” runat=”server”

PostBackUrl=”/Chapter5/cookiedAppB/ReceivePostFromAnotherApplication.aspx”

Text=”Button” /> </div>

<input id=”Hidden1” type=”hidden” runat=”server” />

</form>

</body>

</html>

232

Forms Authentication

This page markup takes advantage of a new feature in ASP.NET 2.0 called cross-page postings. Although this sample application is not showing the primary purpose of cross-page posting (which is posting between two different pages within the same application), it turns out that you can use cross-page posting just as well to make it easier to post form data across applications. The markup above has set the PostBackUrl property on a standard Button control to a URL located in the second sample application. By doing so, ASP.NET injects some extra information into the page that causes the page to post back to the second application.

In addition to using cross-page posting, the code-behind for the page sets some values for the hidden control that is on the page:

protected void Page_Load(object sender, EventArgs e)

{

this.Hidden1.ID = FormsAuthentication.FormsCookieName; this.Hidden1.Value =

FormsAuthentication.Encrypt(((FormsIdentity)User.Identity).Ticket);

}

The hidden control has its ID set to the same value as the forms authentication cookie. This is necessary because when the request flows to the second application, one of the places the FormsAuthenticationModule will look for a forms authentication ticket is in Request.Form[“name of the forms authentication cookie”]. The value of the hidden control is set to the encrypted value of the FormsAuthenticationTicket for the current user. This is the same operation we saw earlier for the redirection scenarios, with the difference being that in this sample the forms authentication ticket is being packaged and stored inside of a hidden form variable rather than a query-string variable.

When you request this page from the first application in the browser, viewing the source shows how everything has been lined up for a successful cross-page post. An abbreviated version of the <form /> element is shown here:

<form method=”post” action=”PostToAnotherApplication.aspx” id=”form1”>

<input type=”hidden” name=”__VIEWSTATE” id=”__VIEWSTATE” value=”/wEPDwUKMTUyMjMyNTkyOWRk/xqxNcEwAvNgbY4ERISdsKcovBo=” />

<input name=”txtSomeInfo” type=”text” id=”txtSomeInfo” /><br />

<input id=”Button1” type=”submit” name=”Button1” value=”Button” onclick=”javascript:WebForm_DoPostBackWithOptions(new

WebForm_PostBackOptions(‘Button1’,’’,false,’’, ‘/Chapter5/cookiedAppB/ReceivePostFromAnotherApplication.aspx’,false,false))” />

<input name=”.ASPXAUTH” type=”hidden” id=”.ASPXAUTH” value=”8CA4D2EB5407E67A6E9950337562ABDEDDBA305644DB3E4B51490F715B4D313A275CE9FB6912 7BE6780462B6570DF8347F282E8FA25E28B1958B13FD710EDF956BD315E40F64B4D44FE3534BA857BA2 F99225E63EA4E65FD40357D995DA1E3F8E4C4D7BAA6E8A4CFC828D357EECEDC27” />

</form>

The forms authentication ticket is packaged up in the hidden form variable. You can also see that the form’s action is set to PostToAnotherApplication.aspx, which at first glance doesn’t look like a page in another application. The form will actually post to another application because the button on the form

233

Chapter 5

has a click handler that calls WebForm_DoPostBackWithOptions. This method is one of the many ASP.NET client-side JavaScript methods returned from webresource.axd (webresource.axd is the replacement for the JavaScript files that you used to deploy underneath the aspnet_client subdirectory back in ASP.NET 1.1 and 1.0).

When you press the button on this, page two things occurs:

1.The WebForm_DoPostBackWithOptions client-side method sets the action attribute on the client-side form to the value /Chapter5/cookiedAppB/ ReceivePostFromAnotherApplication.aspx.

2.The client-side method returns, at which point because the button is of type “submit,” the client-side form is submitted by the browser, using the “action” that was just set.

As a result of this, you have a form-submit from a page in Application A flowing over to application B. When the request hits application B, it starts running through the HTTP pipeline. The FormsAuthenticationModule sees the request, and attempts to find a forms authentication ticket. Eventually, the module looks in Request.Form[“.ASPXAUTH”] for a forms authentication ticket. Because there is a hidden field on the form called .ASPXAUTH, the module is able to find the string value stored there. The module then converts the string value into a forms authentication ticket and sets a cookie on the response that contains this ticket.

At this point the request continues to run, which in the case of the sample application results in a call on the page to:

Response.Write(“The posted value was: “ + Request.Form[“txtSomeInfo”]);

If you run the sample application, you will see that the preceding line of code will successfully play back to you whatever value you typed into the text box back in application A. The other nice thing about this approach is that not only are posted variables retained across the two applications, when you end up on the page in the second application there isn’t the somewhat odd (maybe unsettling?) behavior of the authentication ticket showing up in the address bar of the browser. Additionally, if you view the source of the second page in the browser, there isn’t any authentication ticket there either. For both of these reasons, when running sites with cookie-based forms authentication, POST-based transfers of control between applications are preferred to the approach that relies on calling Response.Redirect.

One last comment on the cross-page posting case: remember that you always need to explicitly set the keys in the <machineKey /> element for all participating applications. Without this, the forms authentication ticket in the hidden field will not be decryptable in the second application.

Cookie-based “SSO-Lite”

Now that you have seen the various permutations of passing forms authentication tickets between applications, let’s tie the concepts together with some sample applications that use a central login form. This approach is conceptually similar to how Passport works with all tickets being issued from central login application. Note that this design only works with cookie-based forms authentication because it relies on issuing forms authentication cookies that can authenticate the browser back to the original application. Websites that use cookieless forms authentication need more explicit code inside of each application due to the need to manually create some approach for hopping authentication tickets from one application to another.

The general design of our “hand-rolled” single sign-on solution is shown in Figure 5-4.

234

Forms Authentication

Browser User

Step 8: User access

Step 1: Attempt to access another application. secured pages in Application A

Step 4: Central login app sends back login form.

Step 2: App A redirects to local login page

Application A

Local Login.aspx

Step 3: Local login page redirects to central login app

Step 5: Browser user posts back credentials

Application B

Step 9: App B redirects to local

login page

Step 6: Central login page redirects to self.

Local Login.aspx

Central login management

Step 7: Central login page redirects back to app A with credentials on the query string

Central Login.aspx Step 10: Local login page redirects to

central login app.

Step 11: Central login app detects user already logged in. Issues ticket on query string and redirects back to app B.

Figure 5-4

235

Chapter 5

The desired behavior of the solution is described in the following list:

1.A user attempts to access a secured application, in this case Application A. At this point, the user has not logged in anywhere and thus has no forms authentication tickets available.

2.When the request is reaches application A, it detects that that application allows authenticated users only. As a result, it redirects the browser to a login page that is local to the application.

3.The local login page does not actually send back a login form to the user at this point. Instead, the local login form places some information onto the query-string and then redirects to a central login application.

4.The central login application detects that the user has never logged in against it, and so it re.directs the user to a login page in the central login application. This is the only point at which the browser user ever sees a login UI.

5.At this point the browser user enters credentials into a form and submits the form back to the central login application.

6.Assuming that the credentials are valid, the login page in the central login application redirects back to itself. This is because the login page handles both interactive logins and noninteractive logins.

7.When the login page redirects to itself, it detects that the user already has a valid forms authentication ticket for the central login application. So instead, the login page clones the forms authentication ticket and sends this new ticket by way of a redirect back to application A. In Application A, the FormsAuthenticationModule will see the ticket on the query-string, convert it into a cookie, and then start running the original page that the user was attempting to access back in step 1.

8.Some time later, the user attempts to access a secured page in application B.

9.Because there is no forms authentication ticket for application B, it redirects to the local login page. As with application A though, the local login page just exists to place information on the query-string and redirect to the central login application.

10.When the redirect reaches the login page in the central login application, the forms authentication ticket issued back in step 6 will flow along with the request. As a result, the login page detects that the user already logged in.

11.Rather than sending back a login form, the login page creates another clone of the forms authentication ticket and places it on a query-string. It then redirects back to application B.

12.The FormsAuthenticationModule in application B converts the forms authentication ticket on the query-string into a forms authentication cookie. The original page that the user requested back in step 8 then runs.

You can see that the primary underpinning of the SSO-lite solution in forms authentication is the ability to pass forms authentication tickets across disparate applications. A website user logs in against a central application, which results in a forms authentication cookie being sent to the user’s browser. That forms authentication ticket becomes the master authentication ticket for all subsequent attempts to access other sites.

Whenever a participating website redirects back into the central login application, the master forms authentication cookie is sent by the user’s browser to the login page in the central application. The central login page can then crack open this ticket and extract most of the values in it, and create a new forms authentication ticket. The new ticket is what is packaged on the query-string and sent back to the original application by way of a redirect.

236

Forms Authentication

The benefit of generating application-specific forms authentication tickets off of the central application’s forms authentication ticket is that all participating applications receive a forms authentication ticket with a common set of issue and expiration dates. It is the central login application that defines for how long the master ticket is valid (and for that matter if sliding expirations are even allowed). The cloned tickets for all of the participating applications simply reflect these settings as established in the central login application.

Now that you have reviewed the conceptual design, it’s time to drill into the actual implementation. There are two important pieces of information that all participating applications need to send over to the central application:

The URL of the page that was originally requested in the application

The desired cookie path that should be used when creating a forms authentication ticket in the participating application

The first piece of information is pretty intuitive — because you want your SSO-lite solution to roughly mirror the standard forms authentication behavior, we need the website user to eventually end up on the page that was originally requested. However, the second piece of information is very important to get right because the solution will be issuing forms authentication tickets in one place (the central login application), but the ticket needs to be converted into a valid cookie in a completely different place (the FormsAuthenticationModule of the participating application).

It turns out that the login in forms authentication for handling cross-application redirects is dependent on the CookiePath property of FormsAuthenticationTicket. When a FormsAuthenticationModule receives a ticket on the query-string, it doesn’t look at the path attribute set in the <forms /> element for the application. Instead, when the module cracks open the ticket that was sent on the query-string, it uses the CookiePath that it finds there as the value for the Path property on the resulting forms authentication

HttpCookie.

In our SSO-lite solution, the two necessary pieces of information are passed from participating applications to the central login application with two query-string variables:

CustomCookiePath — Each participating application sets this value to FormsAuthentication

.CookiePath. That has the effect of ensuring the forms authentication ticket issued inside of each application actually uses the path as set in each application’s configuration.

CustomReturnUrl — Each participating application sets this value to the original URL that the website user was attempting to access. The central login application eventually issues a redirect back to this URL.

For those of you that poke around a bit in the internal workings of forms authentication, you may be wondering why the solution needs a custom definition of a return URL. Whenever forms authentication performs its automatic redirect-to-login-page logic, there is a query-string variable called ReturnUrl. You cannot overload this query-string variable for the purposes of cross-application redirects because forms authentication only places a server-relative virtual path into this variable. Forms authentication does not have the ability in ASP.NET 2.0 to add the DNS or servername into the ReturnUrl variable (that is, forms authentication never prepends http://some.server.address.here/ to this variable).

An SSO-lite solution wouldn’t be very useful though if the only return URLs sent to the central login application were to other applications deployed on the same IIS server. In fact, if that were the only problem you were trying to solve, chances are all you would need to do is set the domain attribute in configuration.

237

Chapter 5

As a result, the SSO-lite solution uses the CustomReturnUrl variable to hold the fully qualified address of the original page the website user was attempting to access. This ensures that the central login application can exist in a completely different DNS namespace from any of the participating applications.

Sample Participating Application

The web.config for a participating application is defined as shown here:

<configuration xmlns=”http://schemas.microsoft.com/.NetConfiguration/v2.0”> <appSettings>

<add key=”centralLoginUrl” value=”http://demotest/Chapter5/CentralLogin/Login.aspx”/>

</appSettings>

<system.web>

<machineKey

decryptionKey=”A225194E99BCCB0F6B92BC9D82F12C2907BD07CF069BC8B4”

validationKey=”6FA5B7DB89076816248243B8FD7336CCA360DAF8”

/>

<authentication mode=”Forms”> <forms loginUrl=”Login.aspx”

cookieless=”UseCookies” enableCrossAppRedirects=”true” path=”/Chapter5/AppAUsingCentralLogin” slidingExpiration=”False”

/>

</authentication>

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

</authorization>

</system.web>

</configuration>

The bolded portions of the configuration require some explanation. First, the <appSettings /> variable defines the full URL needed to reach the login page in the central login application. You would need to set this in the configuration of every participating application so that applications know where to send the authentication redirect to. The enableCrossAppRedirects setting is necessary so that the FormsAuthenticationModule inside of the application will look in the query-string or form post variables for a ticket. With this setting turned on, the participating application can successfully convert tickets send from the central application back into an application-specific forms authentication ticket.

Last, note that slidingExpiration is set to false. Because the central login application issues the master forms authentication ticket, it is the timeout and slidingExpiration settings of the central login application that take precedence. You don’t want participating applications to be renewing forms authentication tickets — rather you want the central login application to do this for you.

Because the configuration above denies access to all anonymous users, any attempt to access a page in the application results in a redirect to the local login page. The local version of Login.aspx is shown here:

protected void Page_Load(object sender, EventArgs e)

{

Redirector.PerformCentralLogin(this);

}

238

Forms Authentication

It is intentionally kept simple because you don’t want to duplicate the redirection login in every single application. In this case, there is a static helper class called Redirector that has a single helper method called PerformCentralLogin.

public static class Redirector

{

//snip....

private static string centralLoginUrl;

static Redirector()

{

centralLoginUrl = ConfigurationSettings.AppSettings[“centralLoginUrl”];

//snip...

}

public static void PerformCentralLogin(Page p)

{

string redirectUrl = FormsAuthentication.GetRedirectUrl(string.Empty, false);

//snip...

string baseServer = p.Request.Url.DnsSafeHost;

string customRedirectUrl = “http://” + baseServer + redirectUrl;

p.Response.Redirect(

centralLoginUrl + “?CustomReturnUrl=” + p.Server.UrlEncode(customRedirectUrl) + “&CustomCookiePath=” + p.Server.UrlEncode(FormsAuthentication.FormsCookiePath));

}

}

For simplicity, I placed the static class definition into the App_Code directory of each participating application. In a production application, you would take this one step further and at least compile the code into a bin-deployable assembly, if not the GAC.

When the Redirector class is first used, the static constructor runs. For now, the code snippet shows only part of the work in the static constructor where it fetches the central login URL once for future use. The single parameter to the PerformCentralLogin method is a reference to the current page. This ensures the helper method has access to any request-specific objects necessary to build up the redirect information. The PerformCentralLogin method fetches the redirect URL using

FormsAuthentication.GetRedirectUrl. At this point, calling GetRedirectUrl works because it returns the virtual path to the originally requested page. However, as noted earlier, the path lacks the server information necessary to allow redirects to work against any arbitrary set of servers and DNS namespaces.

Ignoring some other functionality for a second, the method fetches the server portion of the current URL. With both the server’s address, and the virtual path in hand, the method constructs the fully qualified redirect path. The method can now redirect to the central login application’s login page, including the fully qualified return URL in the CustomReturnUrl query-string variable and the correct cookie path information for the forms authentication ticket in the CustomCookiePath query-string variable.

239

Chapter 5

So, the net result of the original call in the Load event of Login.aspx is that the participating application silently constructs and issues a redirect into the central login application. No user interface for login is ever returned by a participating application.

Let’s return the code that was snipped out earlier. The following includes bolded code that shows some additional logic:

public static class Redirector

{

private static Dictionary<string, string> pages; private static string centralLoginUrl;

static Redirector()

{

centralLoginUrl = ConfigurationSettings.AppSettings[“centralLoginUrl”];

//Register page mappings to force correct casing for the cookie //that will eventually be issued.

pages =

new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);

pages.Add(“/Chapter5/AppAUsingCentralLogin/Default.aspx”,

“/Chapter5/AppAUsingCentralLogin/Default.aspx”);

pages.Add(“/Chapter5/AppAUsingCentralLogin/AnotherPage.aspx”,

“/Chapter5/AppAUsingCentralLogin/AnotherPage.aspx”);

}

public static void PerformCentralLogin(Page p)

{

string redirectUrl = FormsAuthentication.GetRedirectUrl(string.Empty, false);

//Fixup the casing of the redirect URL to prevent problems with new cookies //being issued for a request with incorrect casing on the URL.

redirectUrl = pages[redirectUrl];

string baseServer = p.Request.Url.DnsSafeHost;

string customRedirectUrl = “http://” + baseServer + redirectUrl;

p.Response.Redirect(

centralLoginUrl + “?CustomReturnUrl=” + p.Server.UrlEncode(customRedirectUrl) + “&CustomCookiePath=” + p.Server.UrlEncode(FormsAuthentication.FormsCookiePath));

}

}

All of the bolded code deals with a quirk in cookie handling. If you depend on setting the Path property of an HttpCookie, the path information is case-sensitive. For many developers, using forms authentication this isn’t an issue because forms authentication defaults to a path of /. However, when putting together this sample, there were some frustrating moments before realizing that some of the test URLs I was using had incorrect casing compared to the path of the forms authentication cookie.

240

Forms Authentication

If you plan to create your own SSO-lite solution, and if you intend to segment forms authentication tickets between applications through the use of a cookie’s path property, you need to very careful about how URLs are handled in your code. In the case of the sample SSO-lite solution, the bolded code is a simple workaround for ensuring proper casing. The helper class holds a dictionary containing every URL in the application. The trick here is that the dictionary uses a case-insensitive string comparer, and it uses the invariant culture. This means whenever a lookup is made into the dictionary, the key comparison ignores case, and treats culture-sensitive characters in a neutral manner.

When the PerformCentralLogin method runs, it always takes the redirect URL as returned from forms authentication and converts it into the correct casing. The theory here is that if this method is called, it is very likely that is being called due to an end user (like myself) accidentally typing in the wrong casing for a URL in the IE address bar. By performing a lookup into the static dictionary, the method can convert any arbitrary casing on the redirect URL into a URL with correct casing. Because the SSO-lite solution does partition forms authentication tickets with paths other than / (from the configuration a few pages back, the current application we are looking at uses a cookie path of /Chapter5/AppAUsingCentralLogin), it is important to perform this conversion prior to sending the redirect URL to the central login application.

Central Login Application

The configuration for the central login application pretty much mirrors that of the participating applications.

<configuration xmlns=”http://schemas.microsoft.com/.NetConfiguration/v2.0”> <system.web>

<machineKey

decryptionKey=”A225194E99BCCB0F6B92BC9D82F12C2907BD07CF069BC8B4”

validationKey=”6FA5B7DB89076816248243B8FD7336CCA360DAF8”

/>

<authentication mode=”Forms”>

<forms cookieless=”UseCookies” enableCrossAppRedirects=”true” path=”/Chapter5/CentralLogin” slidingExpiration=”true” timeout=”30”/>

</authentication>

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

</authorization>

</system.web>

</configuration>

Unlike the participating applications, the central login application does not register any URL in the <appSettings /> section. In fact, the SSO-lite solution shown here has zero knowledge of any of the other participating applications.

The bolded attributes in the <forms /> element are of interest because these settings not only define behavior for the master forms authentication ticket issued by the central login application, the settings also influence the ticket behavior for the participating application. Of course, enableCrossAppRedirects is set to true because without that there is no way to hop tickets between applications. The path attribute ensures that the forms authentication ticket for the central login application stays in the central login application. This is why I refer to the forms authentication ticket from the central login application as the “master” forms authentication ticket. After it is issued, the cookie never flows to any other application.

241