I have a mature ASP.NET web application using FormsAuthentication (FA) to manage logins. Under certain situations, I would like to redirect the "just logged in" user to a different URL to the one that FA uses. As per standard functionality, FA will redirect to our normal homepage (specified in web.config) unless a redirectUrl was used when it hits a page that requires an authenticated user.
In my system, after the user's username/password is validated I typically use
FormsAuthentication.RedirectFromLoginPage(userName, createPersistentCookie: true); // Also calls SetAuthCookie()
which handles most situations. However, depending on certain conditions (primarily based on the newly logged in user's role) I want to redirect to a different destination. My thoughts for doing this are to call SetAuthCookie() myself and then use Response.Redirect(myUrl, false); and ApplicationInstance.CompleteRequest().
Despite doing this, the very next request comes in using for the URL defined in my tag of web.config.
<authentication mode="Forms">
<forms loginUrl="~/Login" timeout="120" cookieless="UseCookies" defaultUrl="~/?raspberry=true" />
</authentication>
Here is the actual code I am using (if a different url is required, it is specified by the overrideUrl parameter:
internal static void CreateTicket(string userName, string overrideUrl)
{
// Ref: http://support.microsoft.com/kb/301240
if (overrideUrl == null)
{
FormsAuthentication.RedirectFromLoginPage(userName, createPersistentCookie: true); // Includes call to SetAuthCookie()
}
else
{
FormsAuthentication.SetAuthCookie(userName, createPersistentCookie: true, strCookiePath:FormsAuthentication.FormsCookiePath);
HttpContext.Current.Response.Redirect(overrideUrl, false);
HttpContext.Current.ApplicationInstance.CompleteRequest();
}
}
If I pass in a value of /special/path for overrideUrl I would like the next request to come in to be '/special/path'. Instead I am seeing /?raspberry=true
Is something else forcing defaultUrl?
Is there a way to "snoop" into the Response object while debugging to see if a Redirect is already in place? or set a breakpoint whenever it gets set so I can look at the call stack?
EDIT: At the end of my method, the Response object is showing the following properties:
RedirectLocation: "/special/path"
Status: "302 Found"
StatusCode: 302
StatusDescription: "Found"
IsRequestBeingRedirected: true
HeadersWritten: false
which all looks absolutely correct.
Thanks for any advise.
Okay, I can see what is causing it. It is the fact that I am using the Login Web Control and running my code as part of the Authenticate event. Looking at the reference source for the Login Web Control, the Authenticate event is raised by its AttemptLogin method (search for it in ref source). After raising the event and seeing that Authentication was successful, it then goes on to:
Call SetAuthCookie itself (I've already done this myself but presumably the only thing I should be doing in my code is determining if authentication was successful or not, and not messing with AuthCookie or redirects)
Performing a Redirect (overwriting my carefully crafted Redirect)
I'm going to have to figure out a solution as there these methods are private (can't override by inheriting the usercontrol) and there appears to be no option for overring or suppressing the user of it's GetRedirectUrl().
Related
I have an ASP.NET project in Visual Studio 2013, running locally in IIExpress (Version 8.08418.0) for testing.
The main page (adminDefault.aspx) redirects user to Login.aspx to authenticate (which is done silently based on userID that was passed to adminDefault.aspx as a url variable). The login page sends it back to adminDefault.aspx upon successful authentication. Default then loads data into a Gridview.
The whole process takes about 15 seconds.
When I run this from Visual Studio 2013, I get an error "This page can't be displayed" in about 4 seconds. But if I manually hit refresh, after working for a bit, everything comes in fine.
In my code, suspecting a timeout, I implicitly set this long enough to run through everything:
if (!this.IsPostBack)
{
Session.Timeout = 120; // seconds before timeout
try
{
// OnUser should have been set from Login.asp. If it is null, send to Login
MembershipUser onUser = Membership.GetUser();
if (onUser == null)
{
Response.Redirect("/login.aspx", true);
}
else
{
String currentUserName = Membership.GetUser().UserName;
userRoles = Roles.GetRolesForUser(Membership.GetUser().UserName);
}
}
Even if the Session.Timeout is being ignored, the default is 20 Seconds, so I would expect it to work anyway.
So:
1) Why is the first run through failing?
2) Is there a way I can prevent it? Failing that can I catch the page not found, and refresh automatically?
EDIT: This has something to do with the trip to Login.aspx and back. If I remove Response.Redirect("/login.aspx", true);
Then this works as expected. The problem is, I need the Login.aspx for validation. Anyone have any thoughts as to why the trip to Login and back is not working... and then does upon refresh?
When I try to run this in Chrome, I don't get as far. Chrome shows me this:
Hitting the here just cycles for a moment and comes back to the screen.
The Developer Tool on IE are not helpful (at least that i can see) in this case. When I run my project in the debugger, it launches a new IE session. Before I can turn on developer tools and press "Play" to record events, the session has given me "Cannot display the page" error. Of course I can hit "play" in the tools and then refresh, but of course everything runs then without error. The refresh makes everything run right. I need to discover why it fails before then.
EDIT: This is the Login.aspx and Web.Security Code. I didn't write it, but it looks boilerplate:
protected void Page_Load(object sender, EventArgs e)
{
string userName = "";
bool authenticated = FormsAuthentication.Authenticate(ref userName);
if (authenticated)
{
FormsAuthentication.RedirectFromLoginPage(userName, false);
}
}
And the Authentication code looks like this:
/// <summary>
/// Validates a user based on the session id found in the ReturnURL against credentials stored in the ASP.NET membership.
/// </summary>
/// <param name="userName">The user name.</param>
/// <returns>true if the user name and password are valid; otherwise, false.</returns>
public static bool Authenticate(ref string userName)
{
bool Authenticated = Authenticate(ref userName, GetSessionId());
return Authenticated;
}
/// <summary>
/// Redirects an authenticated user back to the originally requested URL or the default URL using the specified cookie path for the forms-authentication cookie.
/// </summary>
/// <param name="userName">The authenticated user name. </param>
/// <param name="createPersistentCookie">true to create a durable cookie (one that is saved across browser sessions); otherwise, false. </param>
/// <param name="strCookiePath">The cookie path for the forms-authentication ticket. </param>
public static void RedirectFromLoginPage(string userName, bool createPersistentCookie, string strCookiePath)
{
System.Web.Security.FormsAuthentication.SetAuthCookie(userName, false, strCookiePath);
// Redirect back to request page.
HttpContext.Current.Response.Redirect(GetRedirectUrl());
}
I think in your initial if statment you are assuming everyone is either logged in or not logged in.But users that are not logged in and people who visit that can also be anonymous (?). What you want to do is secure this admin.Default page and only allow authorized users. Currently your 2 conditions are send back the user to the login.aspx page (that they are currently on) if a user = null or get a role.
Authentication is about knowing who the user is while authorization is all about access to resources .You want only authorized users to access that resource.
I would put the admin.Default.aspx,admin.Default.cs and admin.Default.designer.cs in their own folder called AdminSecure. Then after adding them inside that folder right click and add a Web.config file. In that Web.config what you need to do is deny anonymous users. So they automatically get redirected when they try and access that AdminDefault page.
//denies non logged in users ? == anonymous
<xml version="1.0"?>
<configuration>
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</configuration>
//this can allow particular users
<allow users="User1#mail.com"/>
//deny everyone but one user:
<allow users="User1#mail.com"/>
<deny users="*"/>
comment out your current code hope this helps.
If Login.aspx redirects to adminDefault.aspx before it gets to persist a session to your browser, an infinite redirect loop will happen.
My clues:
1) As you already have a redirect loop - although not intentionally infinite - there is a risk of it becoming infinite under unhandled circumstances
2) you say that a timeout may be occurring, but the server side timeout is ignored: this leads me to think that this happens client side, and in fact could be your browser breaking the redirect loop as a safety measure
3) refreshing helps: this breaks the redirect loop after your session has been persisted client side
4) removing your redirection to adminDefault.aspx helps: this not only breaks the infinite loop, but removes the concept of the redirect loop
A few things you can to do test the redirect loop theory:
1) set a breakpoint on Session.Timeout = 120; and see where things go from there. You'll expect to get to Response.Redirect("/login.aspx", true); once only, but will probably see this line being hit continuously (once per redirection)
or
2) install Fiddler and monitor your redirections.
If the above theory is correct, you have to ensure that Login.aspx gets to persist a session to the browser before redirecting to adminDefault.aspx. I.e., try and remove the redirection to adminDefault.aspx and see if you have gotten a session cookie after redirecting to Login.aspx - if not, you have your suspect. In short, Login.aspx is not done with it's work before you redirect back to adminDefault.aspx - so you have to let it. I can't be more specific without the Login.aspx code.
Based on your application circumstance in the comments, you cannot manage the authentication/authorization via web.config. And, Membership class is usually accessed if you have your custom provider, it's not the default way of accessing logged in users. So do either one of those:
Check the logged in user in your if statement the following way:
if (User.Identity.IsAuthenticated)
{
Response.Redirect("/login.aspx", true);
}
else
{
String currentUserName = Use User.Identity.Name; // accessing the logged in uer
userRoles = Roles.GetRolesForUser(currentUserName); // not sure about this step, have you added your roles to web.Config? or from where you get them
}
Depends on how your ERP integrates with your app (you should be using a custom provider), check where the users get authenticated. (ex. who is calling Membership.ValidateUser(strUsername,strPassword) . And make sure your login.aspx page is updating the session state.
Check this page for more information about implementing a custom provider
I am using Membership provider but how do I get my username and password to login/signin? When I check to see with this code:
if (User.Identity.IsAuthenticated)
// this code always returns false
I have this before when the user uses asp:Login to login:
if (Membership.ValidateUser(email, password))
{
// this is true but what am I missing here to make above not be false?
The so called duplicate question/answer uses SetAuthCookie which according to this (System.Web.HttpContext.Current.User.Identity.IsAuthenticated fails sometimes) causes issues and I need to avoid it.
Use code like below to make it work. The method RedirectFromLoginPage will create the authentication cookie as well as redirect the user to the original page or the default URL (i.e. home page) defined in web config.
if (Membership.ValidateUser(email, password))
{
// Log the user into the site
FormsAuthentication.RedirectFromLoginPage(email, false);
}
Also, make sure that forms authentication is enabled in web config. At a minimum at least set mode="Forms" if not other settings under authentication.
<authentication mode="Forms">
<forms loginUrl="member_login.aspx"
defaultUrl="index.aspx" />
</authentication>
I need to sign out a user when the user closed the tab or browser, how do I do that in ASP.NET MVC?
There are a few things you can do to make sure the user is signed out when the browser is closed, but it depends on how you're setting the FormsAuthentication cookie:
Use Cookieless=True.
Set a FormsAuthenticationTicket to not be persistent
Use FormsAuthentication.SetAuthCookie to set Persistence to false
Use a JavaScript approach to remove the cookie on window.unload.
Cookieless=True approach:
<system.web>
<authentication mode="Forms">
<forms loginUrl="/Account/Login"
protection="All"
cookieless="true" //set to true
</authentication>
</system.web>
This appends the cookie value to the querystring in each request. The problem with this approach is it's not very secure and it messes with SEO. If a user sends anyone the URL they're using, that person can log in as the original user (probably not what you want). As far as 'messing with SEO', it causes the same page to look different to a googlebot based on what URL is passed in. Each QueryString change makes it a new URL, and if anyone uses this for posting a link; it will dilute the search results for a given actual URL.
FormsAuthenticationTicket Approach
When you set an Authentication cookie for the user, set Persistent to False.
If you're doing this in the FormsAuthentication.SetAuthCookie, this is default. If you use the FormsAuthenticationTicket class, you have to specify the cookie expiration.
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1, //version
"blah", //Cookie Name
);
FormsAuthentication.SetAuthCookie() Approach
By default, if you don't set persistent, the authentication cookie will expire at the end of the session (when the user closes the browser).
FormsAuthentication.SetAuthCookie("CookieValue", false); //second argument is persistent'
JavaScript approach:
There are no foolproof methods; all you can do is set the cookie expiration date to before now and hope the user's browser co-operates. If you really, really, really, want the cookie gone, you can always try a JavaScript approach, but that won't work if the user has JavaScript disabled.
window.addEventListener('unload', function(event) {
document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
});
Other caveats
It also matters which browser you use. Chrome has the ability to run in the background, and that keeps Session Cookies around until their timeout is hit -- they are not dropped when the browser is closed (I found this out the hard way).
I have a strange issue.
I have a page with the following code.
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
Server.Transfer(#"~/Views/Public/Unauthorised.aspx");
return;
}
For some reason, with one user (and we've narrowed it down to his single machine and windows logon profile), IsAuthenticated always returns false. Even though he is logged into the website, and can navigate to other pages that require authenticated user. Except this one page.
We checked that the machine accepts all cookies and we still get the same issue. I'm not sure where to go from here... any suggestions?
There are at least two known cases that can make this behavior.
First case when you have set requireSSL="true" on the Authentication session on web.config and you call that function from a non secure page. So double check if the page is secure or not, if you use the requireSSL="true"
Debug.Assert(Request.IsSecureConnection, "The IsAuthenticated will fail.");
if (!HttpContext.Current.User.Identity.IsAuthenticated)
{
Server.Transfer(#"~/Views/Public/Unauthorised.aspx");
return;
}
Second case when you do not have set the domain="site.com" again on authentication session inside the web.config, and you try to request a cookie the one time from the www.yoursitename.com and the other from yoursitename.com. In that case the authentication cookies are different and it will fail. So set that parameter among others on web.config.
<authentication mode="Forms">
<forms domain="yoursitename.com" />
</authentication>
I have implemented my own role provider, and I'm not using the default one. It works to the point that it can tell when someone should or should not be able to view a page.
However, can it do the following:
If a user is not logged in, redirect to my login page
If a user IS logged in but does not have the correct role, redirect to a different page
I haven't figured out how to do this with the Authorize attribute, all I have is:
[Authorize(Roles="Admin")]
Basically I need to redirect to a different page based on what part of the authorization fails.
I've looked to see if it were something in web.config but nothing obvious jumps out.
VoodooChild answered #1.
For #2 -
What you can do is check if the user is logged on the login page and display a different message or an entirely different page (or even do a redirect to a different action).
Alternatively you can create your own authorization attribute. This will require that you use this attribute everywhere instead of the default AuthorizeAttribute
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary
{
{ "action", "ActionName" },
{ "controller", "ControllerName" }
});
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
Update:
Just thought of another method. When a redirect is done to login page from a different page, a querystring ReturnUrl is also passed. So you can also check if it contains something AND the user is authenticated, chances are the user didn't have permission to view that page.
Off the top of my head, if you are using FormsAuthentication then to answer your first question - yes If the user is not Authenticated or logged in then it can be redirected to the log on page:
Make sure you have this in web.config file (not sure if you need anything beside this, will look into it..)
<authentication mode="Forms">
<forms loginUrl="~/AccountController/LogOn" timeout="2880" />
</authentication>
To answer your second question: "If a user IS logged in but does not have the correct role, redirect to a different page"
The way we did this was, we used the System.Web.Security.Roles.GetRolesForUser(username); method to get the Roles and based on this we redirected the user to the correct view, after login.
Hope this helps!