Yet another problem with forms authentication ticket expiring too soon.
I need to use sliding Expiration set to true. I have read forums and understood the problem with the loss of precision, that the ticket only gets updated if the request is made after half of the expiration time only.
The problem:
In my webconfig I have as follows:
<authentication mode="Forms">
<forms timeout="20" name="SqlAuthCookie" protection="All" slidingExpiration="true" />
</authentication>
<sessionState timeout="20" />
<authorization>
The user must only be logged out and redirected to login.aspx, only when there was no request made in the 20 Minute interval. The problem is that users are making requests, and still get thrown to the login page. This should not happen. What I thought of doing, was to reset the SqlAuthCookie manually for each request.
Below is my code. It is called on context.AcquireRequestState.
void context_AcquireRequestState(object sender, EventArgs e)
{
HttpContext ctx = HttpContext.Current;
ResetAuthCookie(ctx);
}
private void ResetAuthCookie(HttpContext ctx)
{
HttpCookie authCookie = ctx.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null)
return;
FormsAuthenticationTicket ticketOld = FormsAuthentication.Decrypt(authCookie.Value);
if (ticketOld == null)
return;
if (ticketOld.Expired)
return;
FormsAuthenticationTicket ticketNew = null;
if (FormsAuthentication.SlidingExpiration)
ticketNew = FormsAuthentication.RenewTicketIfOld(ticketOld);
if (ticketNew != ticketOld)
StoreNewCookie(ticketNew, authCookie, ctx);
}
private void StoreNewCookie(FormsAuthenticationTicket ticketNew, HttpCookie authCookie, HttpContext ctx)
{
string hash = FormsAuthentication.Encrypt(ticketNew);
if (ticketNew.IsPersistent)
authCookie.Expires = ticketNew.Expiration;
authCookie.Value = hash;
authCookie.HttpOnly = true;
ctx.Response.Cookies.Add(authCookie);
}
My questions are:
Is it wrong or an acceptable solution, resetting the cookie on each request?
Why does it still not work? It seems that new Ticket never gets renewed.
Are there other causes possible, for the fact that the users have their forms authentication expired too soon, that I should investigate?
Thank you,
Regards,
A forms authentication cookie only renews itself after half it's expiration time has passed.
From Microsoft:
If the Web page is accessed before half of the expiration time passes,
the ticket expiration time will not be reset. For example, if any Web
page is accessed again at 5:04 00:00:00 PM, the cookies and ticket
timeout period will not be reset.
To prevent compromised performance, and to avoid multiple browser
warnings for users that have cookie warnings turned on, the cookie is
updated when more than half the specified time has elapsed.
This may be your issue. If your clients access your site at the 9 minute mark and don't access it again for 10 minutes they'll be timed out. This occurs even though you have your session timeout set to 20 minutes.
Manually renewing your ticket like you're doing isn't necessary. You just need sliding expiration enabled. If the 'half the specific time' rule doesn't work for you then you'll have to look at other solutions.
Related
I think my sliding expiration is not happening and the people keep getting logged out after just a few minutes. Here is my setup, slidingExpiration is set to "true" and timeout i updated to "60" instead of 20 for testing purposes.
<authentication mode="Forms">
<forms name="Lab.ASPXFORMSAUTH" loginUrl="~/Login" enableCrossAppRedirects="true" cookieless="AutoDetect" domain="lab.org" slidingExpiration="true" protection="All" path="/" timeout="60" />
</authentication>
and here is the login code. If remember me is selected then the ticket expiration time will be one year from nw other wise it will be 20 mins from now.
private static void LoginUser(User user, bool isRememberMe)
{
//Forms Authentication
var expiryDateTime = isRememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddMinutes(20);
var ticket = new FormsAuthenticationTicket(
1, // Ticket version
user.UserId, // Username associated with ticket
DateTime.Now, // Date/time issued
expiryDateTime, // Date/time to expire DateTime.Now.AddYears(1)
isRememberMe, // "true" for a persistent user cookie
JsonConvert.SerializeObject(user.Roles), // User-data, in this case the roles
FormsAuthentication.FormsCookiePath); // Path cookie valid for
// Encrypt the cookie using the machine key for secure transport
var hash = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(
FormsAuthentication.FormsCookieName, // Name of auth cookie
hash); // Hashed ticket
// Set the cookie's expiration time to the tickets expiration time
if (ticket.IsPersistent)
{
cookie.Expires = ticket.Expiration;
}
// Add the cookie to the list for outgoing response
HttpContext.Current.Response.Cookies.Add(cookie);
}
Looks like i have some disconnect going on between the web.config and the ticket expiry time. Do you see what i am doing wrong here? Thanks
Update #1:
Tested the dev site, logged in (FF and chrome) then refreshed the page after 5 mins and it kept me logged in. Then refreshed the page after 14mins and it redirected me to login page.
Tested the prod site (2 servers - load balanced), followed the dev site refresh interval, kept me logged in.
Scott Hanselman has detailed it here.
http://www.hanselman.com/blog/WeirdTimeoutsWithCustomASPNETFormsAuthentication.aspx
You may need to look into iisidle time out
https://technet.microsoft.com/en-us/library/cc771956%28v=ws.10%29.aspx
Got help at asp.net forums to fix the issue.
I'm just creating a simple test between two server. Basically if a user has already authenticated I want to be able to pass them between applications. I changed the keys to hide them
I have three questions:
What is the proper way to validate the cookie across domain application. For example, when the user lands at successpage.aspx what should I be checking for?
Is the below code valid for creating a cross domain authentication cookie?
Do I have my web.config setup properly?
My code:
if (authenticated == true)
{
//FormsAuthentication.SetAuthCookie(userName, false);
bool IsPersistent = true;
DateTime expirationDate = new DateTime();
if (IsPersistent)
expirationDate = DateTime.Now.AddYears(1);
else
expirationDate = DateTime.Now.AddMinutes(300);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
1,
userAuthName,
DateTime.Now,
expirationDate,
IsPersistent,
userAuthName,
FormsAuthentication.FormsCookiePath);
string eth = FormsAuthentication.Encrypt(ticket);
HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, eth);
if (IsPersistent)
cookie.Expires = ticket.Expiration;
cookie.Domain = ".myDomain.com";
Response.SetCookie(cookie);
Response.Cookies.Add(cookie);
Response.Redirect("successpage.aspx");
}
My config:
<authentication mode="Forms">
<forms loginUrl="~/Default.aspx" timeout="2880" name=".AUTHCOOKIE" domain="myDomain.com" cookieless="UseCookies" enableCrossAppRedirects="true"/>
</authentication>
<customErrors mode="Off" defaultRedirect="failure.aspx" />
<machineKey decryptionKey="#" validationKey="*" validation="SHA1" decryption="AES"/>
What is the proper way to validate the cookie across domain application.
For example, when the user lands at successpage.aspx what should I be checking for ?
There shouldn't be anything to check. Forms authentication mechanism will retrieve the ticket from the cookie, check if it is valid. If not present, or invalid, user will redirected to ~/Default.aspx .
This will work provided your cookie matches the configuration of your web.config
Is the below code valid for creating a cross domain authentication cookie ?
I think you shouldn't try to override the settings of your web.config by manually handling the cookie. I think there are better ways for handling cookie persistence (see below for web.config) and you are just implementing a part of the Forms authentication API (loosing web.config for SSL for example )
here, your manual cookie is not HttpOnly : you could for example be subject to cookie theft through XSS
FormsAuthentication has its own way of handling the cookie (see the TimeOut attribute description in http://msdn.microsoft.com/en-us/library/1d3t3c61%28v=vs.80%29.aspx) Your cookie persistence mechanism will be overwritten by this automatic behavior
Your code should just be :
if (authenticated)
{
bool isPersistent = whateverIwant;
FormsAuthentication.SetAuthCookie(userName, isPersistent );
Response.Redirect("successpage.aspx");
}
Do I have my web.config setup properly?
It should be ok for the domain attribute, as long as you want to share authentication among direct subdomains of mydomain.com (it won't work for x.y.mydomain.com), and mydomain.com is not in the public suffix list ( http://publicsuffix.org/list/ )
I would change the timeout and slidingExpiration attributes to :
<forms loginUrl="~/Default.aspx" timeout="525600" slidingExpiration="false" name=".AUTHCOOKIE" domain="myDomain.com" cookieless="UseCookies" enableCrossAppRedirects="true"/>
I guess it is a good way to handle the choice between one year persistent cookies and session cookies. See https://stackoverflow.com/a/3748723/1236044 for more info
I'm working to set up/correct my session timeout code, and have consulted numerous articles like this one and this SO post for ideas on how best to do this. The solution to detecting a session timeout that I continue to see over and over is to first check the Session.IsNewSession property for true, and if so, then check to see if a session cookie already exists. I guess the logic here is that the user has ended their last session that timed out, started a new session, but the old cookie wasn't yet removed. The code for those checks looks like this:
if (Context.Session != null)
{
if (Session.IsNewSession)
{
string szCookieHeader = Request.Headers["Cookie"];
if ((null != szCookieHeader) && (szCookieHeader.IndexOf("ASP.NET_SessionId") >= 0))
{
Response.Redirect("sessionTimeout.htm"); // or whatever code to handle timeout
}
}
}
Now I'm currently working with a Session timeout value of 120 minutes, and a Forms timeout value of 60 mins. Those two lines from my web.config file, resepectively, are here:
<sessionState mode="InProc" cookieless="UseDeviceProfile" timeout="120" />
<authentication mode="Forms">
<forms loginUrl="~/Home/Customer" timeout="60" name=".ASPXAUTH" requireSSL="false" slidingExpiration="true" defaultUrl="~/Home/Index" cookieless="UseDeviceProfile" enableCrossAppRedirects="false"/>
</authentication>
So after 60 minutes (I set it to 1 to test), I make a request to the server, and I'm automatically redirected to /Home/Customer, I assume due to the 'loginURL' value in my web.config line.
The problem is that the session does not end, and all of my session timeout checks are in the Home/Customer action (I use MVC). So I'm redirected to Home/Customer, and I run through the checks above, but when I get to Session.IsNewSession, it's false, because the session is still alive (I assume because I'm still within the 120 minutes I have set).
So, finally, my question. Does this whole session-timeout-checking scheme only work when the actual Session times out? Or can I make it work for Forms timeouts as well? Maybe the solution is to set my Session timeout value as the same as the Forms timeout value?
The ASP.NET session cookie and the Forms authentication cookie are actually completely different cookies - if your app pool recycles, for instance, your user will lose their session but not their login identity (assuming you're using in-proc session). The only way your code would be hit, I think, is if your session timeout was less than your forms timeout. Any other way, and you will be redirected to the login page before you hit the session timeout code.
Another option would be to move the session timeout code to your login page.
A third would be to handle the check within your global.asax.
I just did some test of what user John Christensen said about
if your session timeout was less than your forms timeout.
web.config
<system.web>
<sessionState mode="InProc" timeout="1" />
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" defaultUrl="~/Account/Login" name="MyPortalAuth" timeout="2" />
</authentication>
<compilation debug="true" targetFramework="4.5" />
<httpRuntime targetFramework="4.5" />
</system.web>
And it seems it is working solution.
C# filter attribute
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
public UserManager<ApplicationUser> UserManager { get; private set; }
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var ctx = HttpContext.Current;
// check if session is supported
if (ctx.Session != null)
{
// check if a new session id was generated
if (ctx.Session.IsNewSession)
{
// If it says it is a new session, but an existing cookie exists, then it must
// have timed out
string sessionCookie = ctx.Request.Headers["Cookie"];
if ((null != sessionCookie) && (sessionCookie.IndexOf("ASP.NET_SessionId") >= 0))
{
if (ctx.Request.IsAuthenticated)
{
FormsAuthentication.SignOut();
}
RedirectResult rr = new RedirectResult(loginUrl);
filterContext.Result = rr;
//ctx.Response.Redirect("~/Account/Logon");
}
}
}
base.OnActionExecuting(filterContext);
}
}
how to check whether users is authenticated and session is valid on pages after say 30 mins.
Assuming you are either hooking into the standard ASP.NET membership providers or using Basic/Digest authentication in IIS then you can easily tell if a user is authenticated using:
if (Request.IsAuthenticated)
{
// User is authenticated, allow them to do things
}
If their authentication token has expired (defaults to 20 minutes with Forms Auth, Windows auth should re-authenticate correctly with each request), then the next time you check that, IsAuthenticated will return false.
You could store a token in the users session that maps back to their user account (either a hash of their user name, or their user id or similar), and then check the two on the request - if they don't match, then the session has become invalid:
// Get the current user, and store their ID in session
MembershipUser user = Membership.GetUser();
Session["UserId"] = user.ProviderUserKey;
To check this:
if (null != Session["UserId"]) {
MembershipUser user = Membership.GetUser();
if (Session["UserId"] == user.ProviderUserKey) {
// User and session are valid
}
}
But to be honest, it depends on what you are trying to do.
If you want to restrict access to certain areas of your website if the user isn't logged in, then there are mechanisms in the configuration that allow for that:
In your web.config you can add lines like the following:
<location path="SecureDirectory">
<system.web>
<authorization>
<deny users="?"/>
</authorization>
</system.web>
</location>
This will deny all anonymous users access to the directory /SecureDirectory/ and all content below it, and direct them instead to your configured login page - for more information on the Authorization element, see "How to: Configure Directories Using Location Settings".
You have to make the session expire after certain time.
So, there is a section in your web.config or you have to add the section in <system.web />
Put this section inside:
<sessionState mode="InProc" stateConnectionString="tcpip=127.0.0.1:42424"
stateNetworkTimeout="10" cookieless="false" timeout="30" />
If you notice we are using InProc mode and timeout to be 30
Now its all up to you.
Add a key in Session object when you can find in any WebForm page.
public void btnLogin(object sender, EventArgs e) {
if (validUser) {
Session["authenticated"] = true;
}
}
and check Session["authenticated"] when required.
Session object will be expired in 30 minutes of session instantiation.
Hope this help. Please feel free to leave me a comment if you face trouble.
In the start of a session, you can store some key value in session state via the Global.asax:
void Session_Start(object sender, EventArgs e)
{
Session["userId"] = userId; // obtained from a data source or some other unique value, etc.
}
Whenever a user makes a page request or postback, on page load of any or all your pages, check if session value is null:
protected void Page_Load(object sender, EventArgs e)
{
if(Session["userId"] == null)
{
Response.Redirect("logout.aspx");
}
// do other stuff
}
If it is, then the session has expired and you can redirect then to logout page or whatever. The timeout interval is defined in your web.config file.
I have a site that works as expected on my development box. That is, the formsauthentication ticket expires after 30 days. This is achieved through the following code
string roles = UserManager.getAuthenticationRoleString(txtUsername.Text);
HttpCookie formscookie = FormsAuthentication.GetAuthCookie(txtUsername.Text, true);
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(formscookie.Value);
FormsAuthenticationTicket newticket = new FormsAuthenticationTicket(1, ticket.Name, DateTime.Now, DateTime.Now.AddDays(30), true, roles, ticket.CookiePath);
HttpCookie newCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(newticket));
newCookie.Expires = DateTime.Now.AddDays(30);
Response.Cookies.Add(newCookie);
I used fiddler to check that the expiration is set properly and I get this
.ASPXAUTH=84AB5430CF4B1C5F9B59C9285288B41F156FCAFA2A169EACE17A7778A392FA69F66770FD8A08FFD06064B00F0BD788FEEC4A5894B7089239D6288027170A642B3B7EB7DB4806F2EBBCF2A82EE20FD944A38D2FE253B9D3FD7EFA178307464AAB4BCB35181CD82F6697D5267DB3B62BAD; expires=Thu, 21-Jan-2010 18:33:20 GMT; path=/; HttpOnly
So I would expect it to expire in 30 days...But it only makes it about 30 minutes.
I have 3 other interesting tidbits about my environment / code
On the production box there are two sites pointing at the same code one for external access and one for internal access
When the I do get the login page because of premature expiration, the .ASPAUTH cookie is still there and sent to the browser
There is some role checking in the global.asax that looks like this
-
protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if (HttpContext.Current.User != null)
{
if (HttpContext.Current.User.Identity.IsAuthenticated)
{
if (HttpContext.Current.User.Identity is FormsIdentity)
{
FormsIdentity id = (FormsIdentity)HttpContext.Current.User.Identity;
FormsAuthenticationTicket ticket = id.Ticket;
// Get the stored user-data, in this case, our roles
string userData = ticket.UserData;
string[] roles = userData.Split('|');
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(id, roles);
}
}
}
}
You'll need to add a machine key tag to web.config file. It's getting regenerated and that causes your premature timeout.
This is similar to the following question:
figuring out why asp.net authentication ticket is expiring
If the problem is that the user on the production box is kicked and has to log in again via FormsAuthentication, it's possible that the problem is IIS related and not .NET.
I've run into issues before where all the timeout settings in the world within the app didn't make a difference and the user was booted far too early.
Check your IIS settings for both the website and the application pool. There are settings in both related to timeouts, etc.
If II6:
Under website properties -> Home Directory tab -> Configuration Button -> Options Tab -> there is session state/length info here
Under application pool for your site -> Performance and Health tabs -> both have several settings that may recycle your pool (and essentially force a re-logon)
To debug, you could disable all health and performance checks on the pool, however be very careful as this could throttle your server if the app gets out of control.
You could also try putting the timeout settings in the web.config:
<system.web>
<authentication mode="Forms">
<forms timeout="XXXXX"/>
</authentication>
</system.web>
Anyway just some ideas from personal experience of similar issues. Hope it might help!