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

Asp Net 2.0 Security Membership And Role Management

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

Chapter 5

Of course, there are myriad side effects with this workaround:

Redirection behavior is still hard to test. You have to laboriously test each page in the site where you may inject a proactive renewal of the forms authentication ticket.

The extra, and potentially unnecessary, redirects make the website seem slower.

The workaround still doesn’t solve the problem of a user entering a checkout process (for example), getting up from the computer, and coming back a little later after more than 50% of the lifetime for his or her current ticket has elapsed. This specific scenario is one where dumping the user back to the page they were just on, with empty fields, is likely to cause the user to bailout of the checkout process.

Unfortunately, there isn’t an elegant solution to the unintended redirect problem with cookieless tickets. The best advice is to turn off sliding expirations, and set the forms authentication ticket lifetime to a “reasonable” value (say somewhere around 30 to 60 minutes).

Sharing Tickets between 1.1 and 2.0

It is likely that most organizations will need to run ASP.NET 2.0 and ASP.NET 1.1 applications side by side for a few years. In many cases, if corporate developers integrate custom internal ASP.NET sites with web-based applications from third-party vendors, they may need to wait for the next upgrade from their vendors before moving a web application over to ASP.NET 2.0.

Although early on during Beta 1 and before there were incompatibilities between the two versions of ASP.NET forms authentication, those issues were ironed out. As a result, you can accomplish both of the following scenarios when running in mixed environments:

You can issue forms authentication tickets from ASP.NET 2.0 applications, and the tickets will work properly when they are sent to an ASP.NET 1.1 application.

You can issue forms authentication tickets from ASP.NET 1.1 applications, and the tickets will work properly when they are sent to an ASP.NET 2.0 application.

To interoperate tickets between the two versions, you must ensure the following:

1.ASP.NET 2.0 must be configured to use 3DES for encryption. Remember that by default ASP.NET 2.0 uses AES for its encryption algorithm.

2.Both ASP.NET 1.1 and ASP.NET 2.0 must share common decryption and validation keys.

The first point was discussed earlier in the section on ticket security. However, the second point may not be immediately obvious for some types of applications. By default, both the validationKey and decryptionKey attributes are set to AutoGenerate,IsolateApps. This holds true for both ASP.NET 1.1 and ASP.NET 2.0. If a developer changed the settings to instead be AutoGenerate, that temporarily solves the problem of sharing the auto-generated key material across multiple ASP.NET applications on the same machine.

222

Forms Authentication

However, when ASP.NET 2.0 is installed on a machine running ASP.NET 1.1 (taht is, aspnet_regiis -I is run), the auto-generated key material is regenerated for ASP.NET 2.0. This means on a single web server that has both ASP.NET 1.1 and ASP.NET 2.0 running, setting any of the key attributes in <machineKey /> to AutoGenerate is not sufficient. If you need to share forms authentication tickets between ASP.NET 1.1 and ASP.NET 2.0, you must use explicitly generated keys, and you must set the key values in the encryptionKey”and decryptionKey attributes of <machineKey />. The section earlier on generating keys programmatically has sample code that makes it easy to generate the necessary values.

To demonstrate these concepts, use two simple applications. Both applications are initially configured as follows:

<authentication mode=”Forms” />

<authorization>

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

Each application has a login page that simply issues a session based forms authentication cookie after clicking a button on the page (interoperating 1.1 and 2.0 only works with cookies because there was no URL-based forms authentication in the base ASP.NET 1.1 product). With this basic web.config, forms authentication tickets will not work between the two applications because the defaults in <machineKey /> are being used. If you try logging in against the 1.1 application and then change the address in the URL to reference a secure page in the 2.0 application, the ASP.NET 2.0 application returns you to the login page for the ASP.NET 2.0 page.

The reason for this is twofold — the keys are different between the two applications, and ASP.NET 2.0 is using AES by default. To rectify this, place a <machineKey /> section into both applications with explicit decryption and validation keys. In the case of ASP.NET 2.0, the <machineKey /> section must also specify the correct encryption algorithm:

<machineKey

decryptionKey=”A225194E99BCCB0F6B92BC9D82F12C2907BD07CF069BC8B4”

validationKey=”6FA5B7DB89076816248243B8FD7336CCA360DAF8”

decryption=”3DES”

/>

decryptionKey is 48 characters long, which is the recommended length when using 3DES (48 characters = 24 bytes = three 8 byte keys of which only 56-bits are used for each of the three keys used in 3DES), validationKey is 40-characters long, which is the minimum length supported by this attribute.

With the updated <machineKey /> sections, you can now log in to the ASP.NET 1.1 application, and then change the URL to reference a 2.0 page without being forced to login again. The reverse scenario also works properly: you can log in to the 2.0 application and then reference a 1.1 page without being forced to log in again.

The only slight difference between tickets issued by ASP.NET 1.1 and ASP.NET 2.0 is the version property. If the forms authentication ticket is generated by ASP.NET 1.1, the FormsAuthenticationTicket

.Version is set to 1. If the forms authentication ticket is generated by ASP.NET 2.0, then the property returns 2. Because neither ASP.NET 1.1 nor 2.0 do anything internally with the Version property (aside from packing and unpacking the value), the different values are innocuous. If for some reason you have business logic that depends on the value of the Version property be aware that in a mixed ASP.NET environment there is no guarantee of a stable value.

223

Chapter 5

Leveraging the UserData Proper ty

I will start out by saying up front that you can only leverage the UserData property for applications that run in cookie mode. Although the constructor for creating a FormsAuthenticationTicket with user data is public, there is no publicly available API for setting an instance of a FormsAuthenticationTicket onto a URL. As a result, the only way that the UserData can be used is if authentication tickets are sent in cookies.

The nice aspect of the UserData property is that after you get custom data into the forms authentication ticket, the information is always there and available on all subsequent page requests. The problem in both ASP.NET 1.1 and ASP.NET 2.0 is that there is no single method that you can call wherein you supply both custom data for the UserData property and the username of the authenticated user. This oversight in ASP.NET 2.0 is somewhat unfortunate because I run across internal and external customers over and over again that need to store a few extra pieces of identification or personalization information after a user logs in. Storing this information in the forms authentication ticket is logical, and it can eliminate the need to cobble together custom caching mechanisms just to solve basic performance problems such as displaying a friendly first name and last name of a customer on every single web page.

So, how do you store extra information in a forms authentication ticket and then issue the ticket in a way that all of the other settings (mainly the issue date and expiration date) are set to the correct values? More importantly, how do you do this without the need to hard-code assumptions into your code around cookie timeouts? In the FormsAuthentication class in ASP.NET 2.0, there is one glaring omission, you can’t retrieve the timeout attribute that is set in the <forms /> element in configuration. Although you can technically retrieve this information with the strongly typed configuration classes in ASP.NET 2.0 (there is a FormsAuthenticationConfiguration class that provides strongly typed access to the values set in configuration), as was discussed in Chapter 4, you cannot use the strongly typed configuration classes when running in partial trust.

The following solution uses a simple workaround to ensure that all of the forms authentication settings are still used when manually issuing a forms authentication ticket, and it does it in a way that will still work in partial trust applications.

protected void Button1_Click(object sender, EventArgs e)

{

HttpCookie cookie = FormsAuthentication.GetAuthCookie(txtUsername.Text, false);

FormsAuthenticationTicket ft =

FormsAuthentication.Decrypt(cookie.Value);

//Cutom user data

string userData = “John Doe”;

FormsAuthenticationTicket newFt = new FormsAuthenticationTicket( ft.Version, //version ft.Name, //username

ft.IssueDate, //Issue date ft.Expiration, //Expiration date ft.IsPersistent,

userData,

224

Forms Authentication

ft.CookiePath);

//re-encrypt the new forms auth ticket that includes the user data string encryptedValue = FormsAuthentication.Encrypt(newFt);

//reset the encrypted value of the cookie cookie.Value = encryptedValue;

//set the authentication cookie and redirect Response.Cookies.Add(cookie); Response.Redirect(

FormsAuthentication.GetRedirectUrl(txtUsername.Text, false),false);

}

Because you need to ultimately issue a forms authentication cookie, the first step is to call FormsAuthentication.GetAuthCookie, passing it the values that you would normally pass directly to FormsAuthentiction.RedirectFromLoginPage. This results in a cookie that has the correct settings for items such as cookie domain and cookie path. It also results in an encrypted cookie payload containing a forms authentication ticket. You can easily extract the FormsAuthenticationTicket by passing the cookie’s Value to the Decrypt method.

At this point, you have a fully inflated FormsAuthenticationTicket with the correct values of IssueDate and ExpirationDate already computed for you. You can create a new FormsAuthenticationTicket instance based on the values of the FormsAuthenticationTicket that was just extracted from the cookie. The only difference is that for the userData parameter in the constructor, you supply the custom data that you want to be carried along in the ticket. In the case of the sample, I just store a first name and last name as an example. Because the user data needs to fit within the limits of a single forms authentication ticket, there are some constraints on just how much information can be stuffed into this parameter.

Internally, when you call FormsAuthentication.Encrypt, a 4K buffer is allocated to hold some of the interim results of encrypting the data. The net result is that that you cannot exceed roughly 2000 characters in the userData parameter if you need to call the Encrypt method. However, because the ultimate result needs to be stored in a cookie, you really only have 4096 bytes available for storing the entire ticket in the cookie. By the time the encryption bloat and hex string conversions occur, the realistic upper bound on userData is around 900–950 characters. This still leaves a pretty hefty amount of space for placing information into the forms authentication ticket. And it is certainly enough space for common uses such as storing first name and last name, or storing a few IDs that are needed elsewhere in the application.

In the sample code shown previously, the new FormsAuthentication instance is encrypted with a call to FormsAuthentication.Encrypt, and the result is placed in the Value property of the cookie that we started with. At this point, you now have a valid forms authentication cookie, with an encrypted representation of a FormsAuthenticationTicket that includes custom data. Notice that nowhere does the sample code need to rely on hard-coded values for determining date-time information. Also, the sample doesn’t call into any configuration APIs to look up any of the configuration values for the forms authentication feature.

The last step in the sample is to add the forms authentication cookie into the response and then issue the necessary redirect. The Response.Redirect call shown in the sample roughly mirrors what occurs inside of that last portion of FormsAuthentication.RedirectFromLoginPage. Note that the

225

Chapter 5

Redirect overload that is used issues a “soft” redirect. The second parameter to the method is passed a false value, which means the remainder of the page will continue to run. Only when the page is done executing, and remainder of the HTTP pipeline completes, will ASP.NET send back the redirect to the browser.

The call to GetRedirectUrl causes the forms authentication feature to find the appropriate value for the redirect URL based on information in the query-string (the familiar RedirectURL query-string variable you see in the address bar when you are redirected to a login page), or in the form post variables. Calling GetRedirectUrl eliminates the need for you to write any parsing code for determining the correct redirect target.

You can run the sample application by attempting to access a simple home page that displays the UserData property on the ticket.

//Display some user data FormsAuthenticationTicket ft =

((FormsIdentity)User.Identity).Ticket;

Response.Write(“Hello “ + ft.UserData);

As you can see, after you jump through the hoops necessary to set the UserData in the ticket, it is very handy and easy to get access to it elsewhere in an application. Hopefully in future releases, ASP.NET will make it a bit easier to issue tickets with custom data as well as extending this functionality over to the cookieless case.

Passing Tickets across Applications

Another title for this section could be “how to roll a poor man’s single sign-on (SSO) solution.” In ASP.NET 2.0, forms authentication includes the ability to pass forms authentication tickets across applications. Although prior to 2.0 you could create a custom solution that passed the forms authentication ticket around as a string, you had to write extra code to handle hopping the ticket across applications.

ASP.NET 2.0 now supports setting the domain value of the forms authentication cookie from inside of configuration. ASP.NET 2.0 also adds explicit support built into the APIs and the FormsAuthenticationModule for handling tickets that are passed using either query-strings or form posts. As long as you follow the basic conventions expected by forms authentication, the work of converting information sent in these alternative locations into a viable forms authentication ticket is automatically done by ASP.NET.

Cookie Domain

The ASP.NET 2.0 forms authentication configuration section adds a new domain attribute. By default this attribute is set to the empty string, which means that cookies issued by forms authentication APIs will use the default value of the Domain property for a System.Web.HttpCookie. As a result, the Domain property of the cookie will be set to the full DNS address for the issuing website. For example, if a page is located at http://demotest/login.aspx, the resulting cookie has a domain of demotest. On the other hand, if the full DNS address for the server is used in the URL: http://demotest

.somedomain.com/login.aspx. Then the resulting cookie has its domain set to demotest

.somedomain.com.

226

Forms Authentication

In ASP.NET 1.1, this was the only behavior supported by forms authentication, which made it problematic when attempting to share cookies across websites that only shared a portion of the domain name. For instance, you might need to authenticate users to demotest.somedomain.com as well as someotherapp.somedomain.com, but the set of users is the same for both applications.

With ASP.NET 2.0 this is easy to accomplish. Add the domain attribute to the <forms /> element and set its value to the portion of the domain name that is shared across all of your applications.

<forms ... path=”/” domain=”somedomain.com” />

With this setting, each time a cookie is issued by forms authentication the cookie’s domain value will be set to somedomain.com. As a result, the browser will automatically send the cookie anytime you request a URL where the network address ends with somedomain.com. Another nice side effect of this new support in ASP.NET 2.0 is that renewed forms authentication cookies (remember that with sliding expirations enabled, cookies can be renewed as they age) will also pick up the same value for the domain. In ASP.NET 1.1, if you enabled sliding expirations but you manually issued the forms authentication cookie with a different domain than the default, it was possible that the cookie would be automatically renewed by the FormsAuthenticationModule. When that happened in ASP.NET 1.1, it reissued the cookie and never set the domain attribute on the new cookie.

Cross-Application Sharing of Ticket

The ability to customize the domain of the forms authentication cookie is useful when all of your applications live under a common DNS namespace. What happens though if your applications are located in completely different domains? Companies that support multiple web properties, potentially with different branding, have to deal with this. The URLs of public websites are frequently chosen so as to be easy to remember for customers and, thus, are not necessarily chosen for purposes of DNS naming consistency. ASP.NET 2.0 introduces the ability to share forms authentication tickets across arbitrary sites by passing the forms authentication ticket around in the query-string or in a form post variable. This new capability allows developers to intelligently flow authentication credentials across disparate ASP.NET sites without forcing a website user to repeatedly login.

Prior to ASP.NET 2.0 your only options were to manually create some type of workaround for this or to purchase a third-party vendor’s single sign on (SSO) product. A number of developers though really don’t need all of the complexities and costs of full-blown SSO products. If the problem that you need to solve is primarily centered on sharing forms authentication tickets across multiple ASP.NET websites with different DNS namespaces, then the support for passing forms authentication tickets across applications in ASP.NET 2.0 will be a good fit.

That leads to the question of when wouldn’t you use the new cross application capabilities in ASP.NET 2.0? There are still valid reasons for using true SSO products, some of which are listed below:

1.You need to share authenticated users across heterogeneous platforms. For example you need to support logging users in across UNIX-based websites and ASP.NET sites. Clearly forms authentication won’t help here because there is no native support for the forms authentication stack on other web platforms than ASP.NET.

2.You need to share authenticated users across different untrusted organizations. This is a scenario where loose “federations” of different organizations need some way for website customers to seamlessly interact with different websites, but need to do so in a way that doesn’t force the customer to constantly login. For example, maybe a company wants the ability for a

227

Chapter 5

website customer to seamlessly navigate over to a parcel-tracking site to retrieve shipment information, and then over to a payment site to see the status of purchases and payments. Because each site is run by a different company, it is very hard to solve this problem today.

There are a number of companies, including Microsoft, working on SSO solutions that can interoperate in a way allowing for a seamless authentication experience for this type of problem.

3.You may need to map the credentials of a logged-in user to credentials for other back-end data stores. For example, after logging in to a website the user may also have credentials in a mainframe system or a back-end resource planning system. Some SSO products support the ability to map authentication credentials so that a website user logs in once and then is seamlessly reauthenticated against these types of systems.

As you can see from this partial list, most of the SSO scenarios involve more complexity in the form of other companies or other systems that are external to the website. Many extranet and internet sites don’t need to solve these problems, or can live with comparatively simple solutions for reaching into back-end data stores. For these types of sites, the cross-application support in forms authentication is a lower cost and easier solution to the single sign on problem.

How Cross-Application Redirects Work

By default, the “SSO-lite” functionality in ASP.NET 2.0 is not enabled. To turn it on, you need to set the enableCrossAppRedirects attribute to true:

<forms ... enableCrossAppRedirects= “true “ />

Doing so turns on a few pieces of logic within forms authentication. First, the FormsAuthentication

.RedirectFromLoginPage method has extra logic to automatically place a forms authentication ticket into a query-string variable when it detects that it will be redirecting outside of the current application. Second, the FormsAuthenticationModule will look on the query-string and in the form post variables for a forms authentication ticket if it could not find a valid ticket in the other standard locations (that is, in a cookie or embedded in the URL for the cookieless case).

Because cookie based tickets automatically flow across applications that share at least a portion of a DNS namespace, you really only need to set enableCrossAppRedirects to true for the following cases:

You need to send a forms authentication ticket between applications that do not share any portion of a DNS namespace. In this case, the “domain” attribute isn’t sufficient to solve the problem.

You need to send a cookieless ticket between different applications — regardless of whether or not the applications share the same DNS namespace. Cookieless tickets by their very nature are limited to only URLs in the current application.

Cookieless Cross-Application Behavior

Examine the cookieless case first. You can create two sample applications and in configuration set up forms authentication and the authorization rules as follows:

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

</authentication>

<authorization>

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

228

Forms Authentication

<machineKey

decryptionKey=”A225194E99BCCB0F6B92BC9D82F12C2907BD07CF069BC8B4”

validationKey=”6FA5B7DB89076816248243B8FD7336CCA360DAF8”

/>

With this configuration, both applications are forced to use cookieless tickets. Additionally, both applications share common key information which ensures that a ticket from one application is consumable by the other application.

To focus on the cross-application redirect issue, we will keep the rest of the application very simple. Both applications will have a default.aspx page, and a login page. Both login pages (for now) will simply issue a forms authentication ticket for a fixed username and then pass the user back to the original requesting URL:

FormsAuthentication.RedirectFromLoginPage(“testuser”, false);

After you end up on default.aspx, there is a button which you can click to redirect yourself over to the other application:

Response.Redirect(“/Chapter5/cookielessAppB/default.aspx”);

The preceding code is in the sample application called cookielessAppA, so default.aspx redirects over to the other sample application: cookielessAppB. If you were to run both sample applications, and try to seamlessly ping-pong between the two applications, you would find yourself constantly logging in. The culprit of course is that Response.Redirect that punts you to the other application; when that redirect is issued, the cookieless credentials embedded in the current URL are lost.

Unfortunately, you can’t just call one API or use some new parameter on the Redirect method to solve this problem when running in cookieless mode. Although FormsAuthentication.RedirectFromLoginPage has logic to store a ticket on the query-string, the scenario above is one where you click on a link inside of one application, and it takes you over to a second application. For this case, you need a wrapper around Response.Redirect that includes the logic to pass the forms authentication ticket along with the redirection.

I created a simple query-string wrapper:

public static class RedirectWrapper

{

public static string FormatRedirectUrl(string redirectUrl)

{

HttpContext c = HttpContext.Current; if (c == null)

throw new InvalidOperationException(“You must have an active context to perform a redirect”);

//Don’t append the forms auth ticket for unauthenticated users or //for users authenticated with a different mechanism

if (!c.User.Identity.IsAuthenticated || !(c.User.Identity.AuthenticationType == “Forms”)) return redirectUrl;

//Determine if we need to append to an existing query-string or not string qsSpacer;

229

Chapter 5

if (redirectUrl.IndexOf(“?”) > 0) qsSpacer = “&”;

else

qsSpacer = “?”;

//Build the new redirect URL string newRedirectUrl;

FormsIdentity fi = (FormsIdentity)c.User.Identity; newRedirectUrl = redirectUrl + qsSpacer +

FormsAuthentication.FormsCookieName + “=” + FormsAuthentication.Encrypt(fi.Ticket);

return newRedirectUrl;

}

}

Given a query-string, the static method FormatRedirectUrl makes a few validation checks and then appends a query-string variable with the forms authentication ticket to the URL. If the current request doesn’t have an authenticated user, or if it’s not using forms authentication, calling the method is a no-op. Assuming that there is a forms-authenticated user, the method determines whether or not it needs to add a query-string to the current URL, or if instead it just needs to a append a query-string variable (there may already be one or more query-strings on the URL, hence the need for check for this condition).

Last, the method reencrypts the current user’s forms authentication ticket back into a string, and it places it on the query-string. Notice how the value of FormsAuthentication.FormsCookieName is used as the name of the query-string variable. Even though the code isn’t really sending a cookie, the FormsCookieName is the identifier used for a forms authentication ticket regardless of whether the ticket is in the query-string, in a form post variable or contained in a cookie.

To use the new helper method, we can rework the previous redirect logic to look like this:

Response.Redirect(RedirectWrapper.FormatRedirectUrl(“/Chapter5/cookielessAppB/defau

lt.aspx”));

You can update both sample applications to include the new helper class in their App_Code directories. Also, update the forms authentication configuration to enable cross-application redirects. This is necessary for the forms authentication module to recognize the incoming ticket on the query-string properly.

<forms cookieless=”UseUri” enableCrossAppRedirects=”true” />

Now when you use both applications, you can seamlessly ping-pong between both applications without being challenged to log in again. Each hop from application A to application B results in a redirect underneath the hood that includes the ticket on the query-string:

http://localhost/Chapter5/cookielessAppB/default.aspx?.ASPXAUTH=F2CB90DA66DE1044FEE

E4FE676AB6C1226EF04F5FDE104002CEA29448E2CC0CD3AF7BA33E4022C5E786BAD23F98163F708AB21

A528939502ADBCAB5031C918F47AD1A317AC183883

The FormsAuthenticationModule detects this and properly converts the query-string variable back into a cookieless ticket embedded on a URL. Due to the reliance on redirect behavior, you can’t post any data from one application to the other. Instead, you have to pass information between applications with query-string variables. Even if you attempt to use a form post as a mechanism for transferring from one application to another, you can’t avoid at least one redirect. When the FormsAuthenticationModule in

230

Forms Authentication

the second application issues a forms authentication ticket based on the ticket that was carried in the query-string, the module issues a redirect to embed the new ticket onto the URL. The only way to avoid a redirect in this case is if you run in cookie mode, which we shall see shortly.

As an aside, there is one slight quirk exists in how this all works. Remember earlier in the discussion on cookieless tickets where it was mentioned that the requireSSL attribute in the <forms /> element is ignored when using cookieless tickets? If you enable cross application redirects, the requireSSL attribute still affects the FormsAuthenticationModule. Under the following conditions, the FormsAuthenticationModule will ignore any query-string or forms variable containing a ticket:

The requireSSL attribute is set to true.

The module could not find a ticket either in a cookie or embedded in a URL, and hence reverted to looking in the query-string and forms variable collection.

The current connection is not secured with SSL.

If you think you have cross-application redirects setup properly, and you are still being challenged with a login prompt, double-check and make sure that you haven’t set requireSSL to true and then attempted to send the ticket to another application over a non-SSL connection.

Cookied Cross-Application Behavior

You can use a similar application to the cookieless sample to also show cross-application redirects in the cookied case. Again using two sample applications, both applications need to share a common configuration:

<forms cookieless=”UseCookies” enableCrossAppRedirects=”true” path=”/Chapter5/cookiedAppA”/>

<machineKey

decryptionKey=”A225194E99BCCB0F6B92BC9D82F12C2907BD07CF069BC8B4”

validationKey=”6FA5B7DB89076816248243B8FD7336CCA360DAF8”

/>

To simulate isolation of the forms authentication cookies, each application explicitly sets the path attribute as shown above. Because this sample uses cookies, the path attribute prevents the browser from sending the forms authentication cookie for one application over to the second application. Remember that setting the path attribute only takes effect when using cookied modes — for example, setting the “path” attribute would have no effect on the previous cookieless example. For starters, we will use the same redirection helper as we did earlier, and pages in both applications will issue a Response

.Redirect to get to the second application.

When you run the sample applications, you get almost the same result as the cookieless applications. You can bounce around between applications without the need to log in again. However, one noticeable difference is the lack of a second redirect each time you transition from one application to another. When the FormsAuthenticationModule converts the query-string variable into a forms authentication ticket encapsulated inside of a cookie, it does not need to issue a redirect. Instead, it just sets a new cookie in the response, and the remainder of the request is allowed to execute. As a result, when you transition from application A to application B, the URL in the browser address bar still retains the query-string variable used during the redirect:

231