What are the exact steps required for a cookie to persist after a browser is closed? At the moment I have:
createPersistentCookie set to true on LoggedIn event.
MachineKey specified.
Forms sliding expiration set to true.
As long as the browser is open, the user will stay logged in, but as soon as it's closed, and it doesn't matter for how long, the user will need to log in again. What am I missing?
EDIT:
I went through the article pointed out by marapet (see comments below) and it made me interested in whether the ticket does indeed have IsPersistent flag, which it does. The decrypted ticket looks like this:
System.Web.Security.FormsAuthentication.Decrypt(Request.Cookies[System.Web.Security.FormsAuthentication.FormsCookieName].Value)
{System.Web.Security.FormsAuthenticationTicket}
CookiePath: "/"
Expiration: {19/08/2010 17:27:14}
Expired: false
IsPersistent: true
IssueDate: {19/07/2010 17:27:14}
Name: "alex"
UserData: ""
Version: 2
All the details are correct, and correspond to those I set in LoggedIn event. More over the cookie value I can retrieve from the cookie directly, is identical to this one. Yet as soon as I close the browser, the cookie is lost.
What I have noticed, however, is that the cookie carrying the ticket has it's date reset for some reason. Firstly I can't override settings in web.config, so at the end of LoggedIn event it's Expires property is 4000 minutes after issue date, not a month which I am setting programmatically. Then after page load the cookie I retrieve with FormsAuthentication.FormsCookieName has Expires property of 01/01/0001. I think perhaps this is where the problem lies? Any thoughts would be appreciated.
EDIT#2:
I am changing both title and tags to include session, as it turned out to be relevant for the problem/solution
So I found the solution, eventually. As it turns out, it wasn't the problem with the authentication cookie as such (it was retained correctly, or rather would have been if the handler didn't remove it, having incorrectly decided that a user wasn't logged in based on the missing session). The problem was that the Session cookie was lost, or wasn't identified properly. So the fix was to manually add a session cookie during log on like so:
HttpCookie authCookie = new HttpCookie("ASP.NET_SessionId", Session.SessionID);
authCookie.Domain = ".mydomain.com";
authCookie.Expires = DateTime.Now.AddMonths(1);
Response.Cookies.Add(authCookie);
Now when the browsers opens again the session is identified properly and user session restored.
A persistent forms authentication cookie should not be discarded when the browser closes. It stays valid for the timeout value defined in the web.config.
However, some browsers can be configured to discard all cookies at the end of a session - you may want to check the settings of your browser (FireFox: Tools - options - privacy).
Related
I am using custom forms authentication for an asp.net MVC application, and am having problems with some users seemingly not having cookies. The custom forms authentication method we are using is similar to this - custom forms authentication. Essentially, we create a custom Principal and Identity, serialize it, and store it in the UserData property of the FormsAuthenticationTicket :
Login
MyCustomPrincipal principal = new MyCustomPrincipal(user);
DateTime expiration = DateTime.Now.AddMinutes(30);
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
1,
u.Username,
DateTime.Now,
expiration,
true,
JsonConvert.SerializeObject(principal));
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
cookie.Expires = expiration;
Response.Cookies.Set(cookie);
We then grab the auth cookie in the Application_AuthenticateRequest event of global.asax.
global.asax - Application_AuthenticateRequest
// Get the authentication cookie
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = Context.Request.Cookies[cookieName];
// If the cookie can't be found, don't issue the ticket
if (authCookie == null) return;
// Get the authentication ticket and rebuild the principal
// & identity
FormsAuthenticationTicket authTicket =
FormsAuthentication.Decrypt(authCookie.Value);
MyCustomPrincipal userPrincipal = new MyCustomPrincipal(authTicket.UserData);
DateTime expiration = DateTime.Now.AddMinutes(30);
FormsAuthenticationTicket newAuthTicket = new FormsAuthenticationTicket(
1,
((MyCustomIdentity)userPrincipal.Identity).Username,
DateTime.Now,
expiration,
true,
JsonConvert.SerializeObject(userPrincipal));
authCookie.Value = FormsAuthentication.Encrypt(newAuthTicket);
authCookie.Expires = expiration;
HttpContext.Current.Response.Cookies.Set(authCookie);
Context.User = userPrincipal;
web.config
<authentication mode="Forms">
<forms loginUrl="~/Home/Index" timeout="29" name="MYFORMSAUTH" cookieless="UseCookies"/>
</authentication>
This works fine for the large majority of users, however, there are some users who appear to be getting no authorization cookie set. I did a few tests to add more information to my Elmah error logs to see if I could find out more about the problem.
First, I tried setting some test cookies before and after the setting of the authcookie in the Login method. These cookies did not appear in the Elmah logs, so it appears adding cookies of any kind in this method is not working. However, there are other cookies in the logs, including the ASP.NET_SessionId, a google analytics cookie, and sometimes there are even other cookies I have set at other locations in the application (probably from a previous session)
Second, I tried adding some info to the session from the login action, and including it in the error log if the authcookie was not found on the next action. I included the length of the cookie (the name's length + the encrypted value's length) as well as the time of the attempted login. Both of these are added only if the user's credentials are valid, and the application attempts to add the auth cookie. I do see these values in the error logs being produced. The length is always greater than 0, and I haven't seen any bigger that about 2300, so I don't think size is an issue. And the attempted login is identical to the time that the error occurs - so the session variables should have been set immediately before the error occurred (cookies went missing).
A few more notes -
There doesn't appear to be any browser in particular that seems to be causing the error more (though it is possible it occurs more on mobile browsers)
Again, the site seems to work for the large majority of users, and of course we cannot reproduce the issue
Since I am not seeing the test cookies, I am guessing that for some reason no cookies are being set from the login method at that time (though I can see other cookies set elswhere that would imply previous successful logins)
The http referer in the elmah logs is usually set to the login page, which implies that users are probably not hitting the offending page without logging in (at least some of the time) - the state of the session variables seems to support that assumption
I'm often seeing multiple of these errors in a row (separated by a minute or so) - implying that the issue isn't resolved with repeated login attempts (not sure why it would be)
It appears users who have this issue continue to have the issue. In other words, it doesn't appear to be "luck of the draw" - but something either with the user's account (which the cookie length session variable implies it is serializing correctly), or the client browser.
I've heard of at least one user who was able to log in on a mobile device, but not their desktop
In total the site probably uses 10 or so cookies (including all of the various test cookies that have been added) - before adding the test cookies it used about 4 including the auth cookie. Also, when the bug occurs, there appear to usually only be 2 or 3 cookies in the request, so I don't think number of cookies is an issue.
At this point I'm willing to try almost anything. I tried setting up using the custom identity stored in the session as a backup, but couldn't get that working, so even if anyone has ideas about how to implement that (if possible) it would be appreciated (if this is off topic then I can remove it).
Sorry for the walls of text, but I just wanted to point out all of the potential issues we have investigated and most likely ruled out.
EDIT
It appears there may be another potentially related issue. I'm seeing error logs which lead me to believe that the "IsAuthenticated" attribute of some Identities is being set to false when it should not be. We do initialize this to false, and set it to true after the user answers a security question. When we set it to true, it should update the principle and the authentication ticket / cookie. I'm not sure if this is happening because of some issue with how I am deserializing the custom principal or not.
is server side caching enabled?
is I remember exactly I had similar problem and the cause was the server side caching (misconfigured) and the server side code was not executed but the client reaches the page.
in addition in my side there was a bug (iis bug on caching enabled on dynamic page) that in some situations the session cookie is sent to more than 1 client and this causes unexpected result.
this can explain your non logging behavior and the cookie not present on client.
Regards
So I've sort of given up and decided to use the Session to store my principal, and check it when I don't see the authentication cookie. I can do this somewhat easily by creating a custom Authorize attribute, and checking the session there. I haven't pushed this to production yet, so I'm not 100% sure this will work around the issue, but preliminary testing would suggest it will suffice.
CustomAuthorizeAttribute
public class MyCustomAuthorizeAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
// Get the authentication cookie
string cookieName = FormsAuthentication.FormsCookieName;
HttpCookie authCookie = HttpContext.Current.Request.Cookies[cookieName];
// If the cookie can be found, use the base authentication
if (authCookie != null)
{
base.OnAuthorization(filterContext);
}
else
{
// The cookie is not found, check the session for the principal
var p = HttpContext.Current.Session[FormsAuthentication.FormsCookieName];
if (p != null)
{
// there is a principal object in the session
MyCustomPrincipal principal = (MyCustomPrincipal)p;
HttpContext.Current.User = principal;
// we've loaded the principal, now just do the base authorization
base.OnAuthorization(filterContext);
}
else
{
// there is no principal object in the cookie or the session, the user is not authenticated
HandleUnauthorizedRequest(filterContext);
}
}
}
}
Once we've set the current principal appropriately using the custom authorize attribute, we can then just use the base authorization, so we don't have to worry about implementing that functionality ourselves. The base authorization should check the current principal and authorize based off of that.
I'm not going to mark this as an answer, because it doesn't really solve the underlying issue, but I thought I'd provide it as a potential workaround in case someone else stumbled along a similar issue.
Is there a way I can retain the browser session after the user has closed his browser and then reopens.
The default behavior in asp.Net is that it keeps the Asp.Net SessionId in the browser cookie which expires when we close the browser. On reopen the browser asp.net generates a new SessionId and even if the old session is not expired on the server side we can not retain it.
Can we control the expiration behavior of the session cookie in Asp.Net?
You cannot reclaim the session-id as such, but you can certainly restore some of the predictable part of the session state. If you are using forms authentication, then just read the forms-auth cookie in global.asax session start and re-populate the session objects.
You can control the expiration of forms-authentication cookie manually by creating a persistent cookie by using:
FormsAuthentication.SetAuthCookie(userName, true)
Alternatively, you can fine-tune the expiration by manually changing the cookie:
Dim authCookie As HttpCookie = FormsAuthentication.GetAuthCookie(userName)
Dim authTicket As FormsAuthenticationTicket = FormsAuthentication.Decrypt(authCookie.Value)
Dim newAuthTicket As New FormsAuthenticationTicket(authTicket.Version, authTicket.Name, authTicket.IssueDate, expireDate, authTicket.IsPersistent, userData)
authCookie.Value = FormsAuthentication.Encrypt(newAuthTicket)
authCookie.Expires = newAuthTicket.Expiration
HttpContext.Current.Response.Cookies.Set(authCookie)
Where expireDate specifies when the cookie should expire.
Now in global.asax session start you can check if the returning user is still authenticated (by virtue of persistent cookie previously set):
If HttpContext.Current.User.Identity.IsAuthenticated Then
' Here re-populate the predictable part of session state
' Like user profile etc.
End If
Added after Op insights:
Forms authentication is not being used, and the aim is to be able to just restore the previous session.
In such a case the only option is to persist the existing session by way of a persistent cookie, so that you can retrieve it later. There are some workarounds to achieve this. One of the workarounds is explained here by this blog writer:
http://weblogs.asp.net/imranbaloch/archive/2010/06/09/persisting-session-between-different-browser-instances.aspx
What is happening here is that we intercept two events in the global.asax:
PostRequestHandlerExecute: (Occurs when the ASP.NET event handler finishes execution) In this handler, we create a new cookie (say temp), value of which is assigned the value of current SessionId. We make it a persistent cookie by setting the expires property to the session timeout.
PostMapRequestHandler: (Occurs when ASP.NET has mapped the current request to the appropriate event handler) In this handler, we check the returning user by checking the existence of the "temp" cookie. If found, we update the actual session cookie (ASP.NET_SessionId) with the value of our "temp" cookie; thereby effectively restoring the previous session.
Please note that this is just a workaround. The system is designed to create a new session. All we are doing is to use a few hooks to workaround this by persisting an existing session to retrieve it later. All security implications stand.
At the Least you can retrieve Session information. This can be done easily when you set the 'Mode' to "SqlServer".
You can Query the Database (ASPState) & hence the table (ASPStateTempSessions) where you are storing your Sessions.[ I used persistent storage: -sstype p ]
SELECT TOP 5 [SessionId]
,[Created]
,[Expires]
,[LockDate]
,[LockDateLocal]
,[LockCookie]
,[Timeout]
,[Locked]
,[SessionItemShort]
,[SessionItemLong]
,[Flags]
FROM [ASPState].[dbo].[ASPStateTempSessions]
Even if you know your sessionID, you may use it to restore your previous session. Asp.Net will generate a new SessionID when you do a new request.
I am facing a weird issue regarding cookies: I am trying to set up a cookie with the user id the first time the user logs into the application, and next time, if the cookie exists, not require the user authentication again.
For this I am using the code below:
to set the cookie:
HttpCookie userCookie = new HttpCookie("UserCookie");
userCookie.Value = UserId.ToString();
userCookie.Expires = DateTime.Now.AddHours(1);
System.Web.HttpContext.Current.Response.Cookies.Add(userCookie);
to get the cookie:
HttpCookie UserCookie = System.Web.HttpContext.Current.Request.Cookies["UserCookie"];
if (UserCookie != null)
{
// redirect the user to another screen inside the application
}
The weird thing is that my cookie seems not to exist and the user is prompted with the log in screen all the time. When I try to use debugging, it appears to me that the cookie is not null, but it's got an empty string value. What can I do about this?
Many thanks!
Are you trying to GET the cookie in the same code that SETS the cookie? If so, the cookie won't exist. Cookie gets SET on the users browser when the page has been delivered.
Also, can you confirm UserID.ToString() isn't actually an empty string..?
To test properly, set the cookie on page A, then redirect to page B and GET the cookie here.
I have a problem. I have done custom "Remember Me" functionality using cookies.
HttpCookie rememberMeCookie = FormsAuthentication.GetAuthCookie(userName, rememberMe);
if (rememberMe)
{
rememberMeCookie.Expires = Controller.LocalizationProvider.GetAdjustedServerTime().AddMonths(6);
}
HttpContext.Current.Response.Cookies.Add(rememberMeCookie);
I see the cookie in firecookies tools in the Firefox. It exists and has the correct expiration date.
But when I changed time - moved to next month. After that I entered to the site and I unlogged user. If I return to present time - I became authothication user.
May be formsauthentication timeout in your web.config is interferring. Here is what MSDN has to say:
Under ASP.NET V1.1 persistent cookies
do not time out, regardless of the
setting of the timeout attribute.
However, as of ASP.NET V2.0,
persistent cookies do time out
according to the timeout attribute.
I'm creating an HttpCookie, setting only the name and value and not the expires property, then adding it to the response. Simple enough. The cookie is created (but not persisted) as expected. The problem is when the session changes for some reason (like the website was rebuilt, or I rebuilt my app when debugging) then the cookie stays around. I want the cookie to be valid for only the original session it was created on.
According to MSDN it says: "If you do not specify an expiration limit for the cookie, the cookie is not persisted to the client computer and it expires when the user session expires."
I guess I don't know exactly what "session expires" encompasses. I figure the cookie gets deleted after 20 min when the session expires. But should the cookie get deleted if the session it was created on doesn't exist anymore for any number of reasons? The only time I've seen the cookie get deleted is when the user closes all browser windows and opens a new one.
If this is all true, I may have to store the original session id ("ASP.NET_SessionId") in the cookie, then check it against the current session id, if they're different, then delete the cookie or create a new one.
Here's the code (the only difference between my cookie and the one in the MSDN examples is I'm storing multiple values in the cookie):
private void SaveValuesToCookie(string[] names, string[] values)
{
HttpCookie cookie = new HttpCookie("MyCookie");
for (int i = 0; i < names.Length; i++)
{
string name = names[i];
cookie.Values[name] = values[i];
}
Response.Cookies.Add(cookie);
}
private string GetValueFromCookie(string name)
{
HttpCookie cookie = Request.Cookies["MyCookie"];
if (cookie == null)
return null;
return cookie.Values[name];
}
The only time I've seen the cookie get
deleted is when the user closes all
browser windows and opens a new one.
And that is exactly what MSDN means when it says the cookie will be deleted when the session expires. Unfortunately, I believe this isn't consistant across browsers anyway, so it's not much use to anyone.
You should always set an expiry date on Cookies.
If this is all true, I may have to
store the original session id
("ASP.NET_SessionId") in the cookie,
then check it against the current
session id, if they're different, then
delete the cookie or create a new one.
I hate to say it but this isn't going to help you either. The .NET Framework likes to recycle session IDs, so you can't guarantee it will be different.
Bad news out of the way, I would advise you to reconsider what you're trying to do from an architectural standpoint.
Restarting the app is something that happens entirely on the server; cookies are something that happen entirely on the client. While the client will talk to the server, it is purely a Request/Response relationship, the server cannot communicate events such as an application restart to the browser.
If you want to store a value somewhere which is only valid for the lifespan of a server session, why not store it in Session rather than in a Cookie?
I'm aware of all that, but it's not secure enough for our clients needs. The session can still get spoofed or injected. See: http://msdn.microsoft.com/en-us/magazine/cc163730.aspx#S9
Looks like I'm left to creating an expired secure cookie separate of the session, and refreshing the expiration date when I can (ie when the user accesses certain pages).