How to get referrer URL in ASP.NET when there are multiple redirects? - asp.net

I'm developing a web application that uses an in-house SSO server for authentication. I have a link on my home page to a page called Logout.aspx. Logout.aspx clears the Forms Authentication cookie, all session data, then performs a redirect to the LoginUrl specified in the forms authentication configuration which is currently set to a page called Login.aspx.
However when Login.aspx loads, an attempt is made to implicitly reauthenticate the user against the SSO server using the SSO authentication ticket which was previously issued. If this ticket still exists, the previous user will be logged back in and sent back to the home page. I want to determine, when the Login page loads, whether the request has come via the Logout page. The UrlReferrer property of the request still references Home.aspx, presumably because this was the last url the client requested.
Currently I have a workaround in place whereby I append a querystring variable to the request from the logout page that instructs the Login page not to perform an implicit login and instead prompt the user for credentials. How can I determine programmatically whether the request came via a redirect from the Logout page?
Edit 29/04/2009:
Following the conversation with jellomonkey, I should point out that the interaction between the SSO server and the local forms authentication of the consuming website isn't directly relevant to the problem at hand. Expressed succinctly, my problem is:
User clicks HTML hyperlink from Home.aspx which takes them to Logout.aspx
Page_Load event handler of Logout.aspx clears Forms Authentication ticket and Session data and redirects the user to Login.aspx
Page_Load event of Login.aspx checks the UrlReferrer property of the Request object to determine whether the request came via the Logout page. However, in requests which have come via a redirect from Logout.aspx, the UrlReferrer property of the Request object is Home.aspx.
Why is this? Why is the UrlReferrer Home.aspx and not Logout.aspx?

The scenario you are describing should be working correctly unless the logout page is not actually deleting the forms authentication cookie. There are several ways to end the forms authentication session:
//I have seen instances where this does not work.
FormsAuthentication.SignOut()
//I have not seen this code fail before.
Dim cookie As HttpCookie = FormsAuthentication.GetAuthCookie( _
HttpContext.Current.User.Identity.Name, False)
cookie.Expires = Date.Now.AddDays(-1)
Response.Clear()
Response.AppendCookie(cookie)
Response.Redirect(FormsAuthentication.LoginUrl)
Also if you are using a role manager which stores in a cookie remember to call Roles.DeleteCookie().
Edit: In response to the updated question.
The Response.Redirect method does not return a header with a new URL referrer because the spec says that only client initiated requests should contain a referrer header. Here is the Response.Redirect code which you can see does not change the referrer header:
Public Sub Redirect(ByVal url As String, ByVal endResponse As Boolean)
If (url Is Nothing) Then
Throw New ArgumentNullException("url")
End If
If (url.IndexOf(ChrW(10)) >= 0) Then
Throw New ArgumentException(SR.GetString("Cannot_redirect_to_newline"))
End If
If Me._headersWritten Then
Throw New HttpException(SR.GetString("Cannot_redirect_after_headers_sent"))
End If
Dim handler As Page = TryCast(Me._context.Handler,Page)
If ((Not handler Is Nothing) AndAlso handler.IsCallback) Then
Throw New ApplicationException(SR.GetString("Redirect_not_allowed_in_callback"))
End If
url = Me.ApplyRedirectQueryStringIfRequired(url)
url = Me.ApplyAppPathModifier(url)
url = Me.ConvertToFullyQualifiedRedirectUrlIfRequired(url)
url = Me.UrlEncodeRedirect(url)
Me.Clear
If (((Not handler Is Nothing) AndAlso handler.IsPostBack) AndAlso (handler.SmartNavigation AndAlso (Me.Request.Item("__smartNavPostBack") = "true"))) Then
Me.Write("<BODY><ASP_SMARTNAV_RDIR url=""")
Me.Write(HttpUtility.HtmlEncode(url))
Me.Write("""></ASP_SMARTNAV_RDIR>")
Me.Write("</BODY>")
Else
Me.StatusCode = &H12E
Me.RedirectLocation = url
If ((url.StartsWith("http:", StringComparison.OrdinalIgnoreCase) OrElse url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)) OrElse ((url.StartsWith("ftp:", StringComparison.OrdinalIgnoreCase) OrElse url.StartsWith("file:", StringComparison.OrdinalIgnoreCase)) OrElse url.StartsWith("news:", StringComparison.OrdinalIgnoreCase))) Then
url = HttpUtility.HtmlAttributeEncode(url)
Else
url = HttpUtility.HtmlAttributeEncode(HttpUtility.UrlEncode(url))
End If
Me.Write("<html><head><title>Object moved</title></head><body>" & ChrW(13) & ChrW(10))
Me.Write(("<h2>Object moved to here.</h2>" & ChrW(13) & ChrW(10)))
Me.Write("</body></html>" & ChrW(13) & ChrW(10))
End If
Me._isRequestBeingRedirected = True
If endResponse Then
Me.End
End If
End Sub
You can use reflector to follow the other methods but I don't see one which changes any header.

Response.Redirect("login.aspx?from=logout")
Steve Yates
ITS, Inc.
Why doesn't Tarzan have a beard?
~ Taglines by Taglinator: www.srtware.com ~

Related

.Net Owin Authentication Failures... in a bizarre way

I have some "out of the box" .net authentication code:
Dim manager = Context.GetOwinContext().GetUserManager(Of ApplicationUserManager)()
Dim signinManager = Context.GetOwinContext().GetUserManager(Of ApplicationSignInManager)()
Dim result = signinManager.PasswordSignIn(Email.Text, Password.Text, RememberMe.Checked, shouldLockout := False)
Select Case result
Case SignInStatus.Success
IdentityHelper.RedirectToReturnUrl(Request.QueryString("ReturnUrl"), Response)
Exit Select
I have local, Facebook and Google authentication all working.
Now, the VERY strange part.
User 1 logs in, no problem
User 2 logs in... returns "SignInStatus.Success", but "HttpContext.Current.User.Identity" is nothing.
BUT WAIT! There's more!
If I open and save web.config (making no changes) User 2 can now log in!
Now I have 2 users logged in, a third does the same: Can't log in until I save web.config (which would restart the app pool)
So, the question is: WTF is going on here?
The Visual Studio authorization template sets the LoginPath property of the CookieAuthenticationOptions class in Startup.Auth.cs. This handles the ReturnUrl logic and duplicates the functionality in the IdentityHelper.RedirectToReturnUrl method. If this is your problem you'll see 302 responses in your IIS logs when you post to your login page. I fixed this by commenting out the call to IdentityHelper.RedirectToReturnUrl in the Login page.
You can read more about the LoginPath property and what it does here.

Classic ASP: ASPSessionID is reused

I know how to handle this in ASP.NET, but is there a way to force the Classic ASP Session ID to be cleared? It is a randomly generated ID like ASPSESSIONIDG32423E that does not seem to be available in RESPONSE.COOKIES collection thus I can not clear it. We have a class ASP site still hanging around and recently it was an audit finding that after the user logs out the same session ID is reused.
MORE CLARIFICATION:
First visit to page, I see this in the proxy editor in Response:
Set-Cookie: ASPSESSIONID=PUYQGHUMEAAJPUYL; path=/Webapp
After a logout, I call Session.RemoveAll and Session.Abandon and then redirect user to login page. At which point I should see a new Set-Cookie with a different value for SessionID. Instead, I do not get a new cookie and the new login session reuses the original session cookie. This is an audit finding that we have to resolve in some way but there does not seem to be a way to control this.
So I did come up with a solution for this as follows. I added two pages called Start.asp and Start2.asp. The original login page was changed to check for a post variable which is now set on Start2.asp, so if login.asp does not see that post variable, it redirects to Start.asp. Start.asp invalidates the ASPSessionID by setting it to 0. The key is using Response.AddHeader "Set-Cookie" in order to do this since Response.Cookies("ASPSESSIONID...") gives an error that you can't access the element:
Code for Start.ASP
<%
If instr(Request.ServerVariables("HTTP_COOKIE"), "ASPSESSIONID") > 0 Then
Dim Allcookies
AllCookies = Split(Request.ServerVariables("HTTP_COOKIE"),";")
For i = 1 to UBound(AllCookies)
If instr(AllCookies(i), "ASPSESSIONID") > 0 Then
Response.AddHeader "Set-Cookie", Left(AllCookies(i),instr(AllCookies(i),"=") -1) & "=0; path=/;secure;httponly"
End if
Next
End if
Response.Redirect("start2.asp")
%>
Next, it calls Start2.asp which looks for all ASPSEssionID cookies and appends Secure; httponly (I had to add these for another finding, ASP metabase setting to add secure only works if the SSL cert. is on the web server. In our case the SSL cert is on a load balancer in front of the web server).
Code for Start2.asp
<%
'CODE for authorization/authentication
'...
Session.Contents.RemoveAll
Session.Abandon
If instr(Request.ServerVariables("HTTP_COOKIE"), "ASPSESSIONID") > 0 Then
Dim Allcookies
AllCookies = Split(Request.ServerVariables("HTTP_COOKIE"),";")
For i = 1 to UBound(AllCookies)
if left(Request.ServerVariables("HTTP_HOST"),2) = "65" and instr(AllCookies(i), "ASPSESSIONID") > 0 Then
Response.AddHeader "Set-Cookie", AllCookies(i) & "; path=/;secure;httponly"
End if
Next
End if
%>
<html>
<body>
<form action="login.asp" method="post">
<input type="hidden" name="start2" id="start2" value="Yes" />
</form>
<script type="text/javascript">
document.forms[0].submit();
</script>
</body>
</html>
Really, though, the new ASPSessionID is not generated until within Start2.asp so that Set-Cookie code for secure and httponly has to also be done in login.asp. So the same code above was copied to the top of login.asp just after this code:
If request.form("Start2") = "" Then
Response.Redirect("start.asp")
End if
IMO - you need to end the session rather than just clear out the session ID. In this case, Session.Abandon is the solution. Ref.: https://devguru.com/content/technologies/asp/session-abandon.html
HTH.
This relates to ASP.NET but describes the behaviour you are seeing in ASP
When you abandon a session, the session ID cookie is not removed from
the browser of the user. Therefore, as soon as the session has been
abandoned, any new requests to the same application will use the same
session ID but will have a new session state instance.
http://support.microsoft.com/?kbid=899918
This behaviour will only occur if using the same browser for the same session, as soon as the browser is closed the session cookie will be lost (providing an explicit expiry date has not been set).
You could try calling Session.Abandon then redirect the user to a page which uses JavaScript to clear all cookies, then redirect to the login page, or whatever page you like.
Clearing all cookies with JavaScript

Store cookie for other site

I have multiple asp.net sites. When a user logs unto one of the sites, I want to store a cookie telling me that a user has logged on. When the user later visits one of the other sites I have, I would like to read the cookie from that site.
AFAIK you neither can read cookies from or write cookies to other sites, so what could a workaround be?
Perhaps making a redirect to http://www.othersite.com/SaveCookie.aspx ?
Give me some ideas :-)
One of our clients has exactly the same requirement (logging into multiple sites on different domains), complicated by the fact that one of the sites requires that the user is logged in to a classic ASP application, a .NET 1.1 application and a .NET 3.5 application running on different hardware, but under the same domain...
We've basically implemented a system of round-robin style redirects, where each domain logs the user in, then bounces them on to the next domain until they return to the original domain at which point they are redirected to their original request.
So (pages and domains changed to protect the innocent):
User requests www.example1.com/page1.aspx
A cookie is set that tells us the user was attempting to access page1.aspx, and the user is sent to the www.example1.com/login.aspx
The user logs in, and is then redirected to www.example2.com/processlogin.aspx?token=EncryptedToken
ProcessLogin.aspx checks for a cookie telling it where to direct the user, if it can't find one, it decrypts the token, logs the user in on example2.com, and then redirects them to www.example1.com/processlogin.aspx?token=EncryptedToken (or example3.com - repeat as required)
As in 4, ProcessLogin.aspx checks for the cookie, finds it, deletes it and redirects the user to /page1.aspx.
If the user later on visits a page on www.example2.com, before the authentication ticket timeout, they will still be logged in on that site as well.
Edit to respond to comment
That depends on how you are making the "request to the other pages". If you make the request from your code behind, what you're doing is effectively setting the cookie on the server, rather than on the users browser.
Cookies need to be issued by the server to the client browser, and that is done in the headers of the page response - so you need to direct the users browser to a page on the other site to issue the cookie from that domain.
You could generate a request to the other page in an IFrame, or try and do it in a self closing pop-up window - but that has other issues like pop-up blockers, flickering windows, etc.
After some investigation we found that a round-robin set of redirects like this was the simplest and most reliable solution.
A very basic code setup:
An .aspx page, containing a Login control, with a method "OnLoggedIn" attached to the LoggedIn event of the control:
void OnLoggedIn(object sender, EventArgs e){
string returnUrl = Request.QueryString["returnUrl"];
// Create new cookie, store value, and add to cookie collection
HttpCookie myCookie = new HttpCookie("WhereTo");
myCookie["ReturnUrl"] = ReturnUrl;
Response.Cookies.Add(myCookie);
// Redirect user to roundtrip login processor on next domain.
// Work out domain as required.
string redirect = GetNextDomain();
// Add encoded user token
redirect += "?token=" + EncodeUserToken();
// Redirect the user, and end further processing on this thread
Response.Redirect(redirect, true);
}
Then on both servers you have ProcessLogin.aspx, that has something like this in it:
protected void Page_Load(object sender, EventArgs e){
// Look for redirect cookie
if (Request.Cookies["WhereTo"]["ReturnUrl"] != null){
// Save value from cookie
string redirect = Request.Cookies["WhereTo"]["ReturnUrl"];
// Delete original cookie by creating an empty one, and setting it
// to expire yesterday, and add it to the response.
HttpCookie myCookie = new HttpCookie("WhereTo");
myCookie.Expires = DateTime.Now.AddDays(-1d);
Response.Cookies.Add(myCookie);
// Redirect the user, and stop processing
Response.Redirect(redirect, true);
}
// Still here, so log in and redirect
string encryptedToken = Request.QueryString["token"];
if (!string.IsNullOrEmpty(encryptedToken)){
// Decrypt token, and log user in
// This will vary depending on your authentication mechanism
PerformLogin(encryptedToken);
}
// Redirect user to roundtrip login processor on next domain.
// Work out domain as required.
string redirect = GetNextDomain();
// Add encoded user token - no need to recalculate, it will be the same
redirect += "?token=" + encryptedToken;
// Redirect the user, and end further processing on this thread
Response.Redirect(redirect, true);
}
You're looking for a Single Sign-On (SSO) solution.
If it's possible for you to host your sites at different subdomains below the same domain, you can save cookies that are shared for the whole domain, e.g.:
"site1.yourdomain.com" and
"site2.yourdomain.com"
can both read cookies saved to the domain "yourdomain.com"
Another alternative is to tell the other site about the login via a request to it, as in your redirect suggestion. You could do this in several ways, e.g. by loading the page in an iframe, sending the data directly from one server to another, and so on. None of these are particularly elegant, though, and in the case of login, as Tomas Lycken says, you should really be going for a proper SSO implementation.

Forms Authentication and POST requests from AJAX

We have an ASP.NET app protected by forms authentication. The app uses MS AJAX heavily to call its web-services.
When the forms authentication times out, and a GET-request happens - all is fine (the user is redirected to a login page).
BUT when the forms authentication times out and a POST-request happens (ajax) - no redirect happens, instead the app returns "401 unathorized" and the browser prompts for username and password (not a login form, but a browsers built-in dialog). Of course entering ANY username/password never helps.
How do I handle this?
UPDATE: After looking with firebug, I just found out that regular POST requests redirect to login fine, it's only web-service calls that throw "401 Unauthorizes".
The difference between a regular request and web-service is URL. Which is "page.aspx" for a regular post-request and "service.asmx/MethodName" for webservices...
Ok, answering my own questin.
After looking into this issue and researching a bit more I found that when a web-app is protected by Forms-Authentication and the user is not authenticated, this is what happens:
If it's a GET-request - the user is
redirected to the login page.
If it's a POST-request to a page - the user is
redirected to the login page.
If it's a POST-request to a web-service - the
user gets 401-unauthorized
Thats how ASP.NET works
And if a web-service is called by AJAX (xmlHttpRequest object) and returns 401 - of course the browser shows a pop-up login box.
Now, what should you do is add some code to Application_PostAuthenticateRequest that will prevent throwing 401 for webservices.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (Request.RequestType == "POST" //if its POST
&& !User.Identity.IsAuthenticated //if user NOT authed
&& !HasAnonymousAccess(Context) //if it's not the login page
)
{
//lets get the auth type
Configuration config = WebConfigurationManager.OpenWebConfiguration("~");
SystemWebSectionGroup grp = (SystemWebSectionGroup)config.GetSectionGroup("system.web");
AuthenticationSection auth = grp.Authentication;
//if it FORMS auth
if(auth.Mode== AuthenticationMode.Forms)
{
//then redirect... this redirect won't work for AJAX cause xmlHttpRequest can't handle redirects, but anyway...
Response.Redirect(FormsAuthentication.LoginUrl, true);
Response.End();
}
}
}
public static bool HasAnonymousAccess(HttpContext context)
{
return UrlAuthorizationModule.CheckUrlAccessForPrincipal(
context.Request.Path,
new GenericPrincipal(new GenericIdentity(string.Empty), null),
context.Request.HttpMethod);
}
I see two solutions:
(1) "Heart beat" mechanism. On each page include a script that will "ping" the server by some dummy ajax request, like:
<script>
setInterval(ping, 60000); // based on comment by John
function ping()
{
$.get('/do/nothing');
}
</script>
This way the session shouldn't expire as long as the browser window is open.
(2) On each ajax request check the status of the response. If the response has "401 unauthorized" code (or any other code different that 200), that means that the session expired and instead of loading the response into some dialog box in the page redirect the user to login page.
Conclusion based on comments:
The best solution would be to combine the two above mechanisms. Heartbeat mechanism will help to keep the session alive as long as the page is displayed in the browser. But in doesn't guarantee that for sure. The connection to the server can be broke and reopened when the session is expired. So you should check the response status anyway.

why does authenticateRequest fire on a page when user is logged out?

My goal is to write a cookie when the user authenticates. We are using a crappy framework that hides its source code and event model so when I use their login control I can't set a session timeout on it!
Anyhow, I am trying to write a cookie when the user is logged in, and then refresh the cookie expire time on subsequent page views (sliding expiration).
So I figured I could initially create the cookie during Application_AuthenticateRequest in teh global.asax but that seems to be firing even when the user hasn't signed in yet.
Is that suppose to be the case?
The Application_AuthenticateRequest fires on each request, but if you are using forms authentication and the user haven't logged in yet, you will find that the User property of the HttpContext (accessed through this.User in the global application class file) evaluates to null, while it will evaluate to an IPrincipal object if the user is logged in.
So you can do something like this:
Private Sub Application_AuthenticateRequest(ByVal pObjSender As Object, ByVal pEaDummy As EventArgs)
If Me.User IsNot Nothing AndAlso Me.User.Identity.IsAuthenticated Then
If Me.Request.Cookies("authCookieName") Is Nothing Then
' Create cookie
Else
' Update cookie
End If
End If
End Sub
where authCookieName is the cookie name.
Yes. The Application_AuthenticateRequest will occur everytime a request hits the website. The AuthenticateRequest as well as doing the authentication will also check and return if Authorisation is to happen for the page. Some pages need to be excluded from authentication and authorisation checks, such as the login page.
For your situation you should also check the page and exclude those that are involved in the login sequence.

Resources