I'm using ASP.NET Session State to keep track of logged in users on my site.
However, one problem I'm running into is that by default ASP.NET session cookies are set to expire when the browser closes.
http://ahb.me/43e
I've tried setting my own ASP.NET_SessionId cookie and modifying the cookie's expiry using something similar to the following code:
Response.Cookies["ASP.NET_SessionId"].Expires = DateTime.Now.AddMonths(1);
None of these approaches work, they all set a second cookie with the same name.
Is there a way of changing the session cookie's expiry?
Based on links in Joe's answer, I figured out this approach:
public void Application_PostRequestHandlerExecute(object sender, EventArgs e)
{
UpdateSessionCookieExpiration();
}
/// <summary>
/// Updates session cookie's expiry date to be the expiry date of the session.
/// </summary>
/// <remarks>
/// By default, the ASP.NET session cookie doesn't have an expiry date,
/// which means that the cookie gets cleared after the browser is closed (unless the
/// browser is set up to something like "Remember where you left off" setting).
/// By setting the expiry date, we can keep the session cookie even after
/// the browser is closed.
/// </remarks>
private void UpdateSessionCookieExpiration()
{
var httpContext = HttpContext.Current;
var sessionState = httpContext?.Session;
if (sessionState == null) return;
var sessionStateSection = ConfigurationManager.GetSection("system.web/sessionState") as SessionStateSection;
var sessionCookie = httpContext.Response.Cookies[sessionStateSection?.CookieName ?? "ASP.NET_SessionId"];
if (sessionCookie == null) return;
sessionCookie.Expires = DateTime.Now.AddMinutes(sessionState.Timeout);
sessionCookie.HttpOnly = true;
sessionCookie.Value = sessionState.SessionID;
}
This code can be inserted in Global.asax.cs.
I would suggest you use FormsAuthentication to track logged in users. You can use a persistent FormsAuthenticationCookie to achieve what you want.
Or if you really want to use Session State, try this technique.
Just a guess: Maybe editing the session.configuration inside the web.config could change the cookie-expiration? You can take a look here?
I think trying to keep session alive for a long time is the wrong approach and limits your scalability. The session cookie is pointing to a specific session that's being maintained by IIS on the server. In general, you want session to close after the user closes their browser so as to not consume all of the available server resources for inactive users. You want session for a departing user to close and the resources made available to a new arriving user. That's why the session cookie expires.
If you want to maintain some user state after the user closes their browser, you can always look at something like Profile. Or, if this is for something like a shopping cart, you can persist your shopping cart in a database and then reconnect that to the user when they log on again.
Tom's answer almost worked for me, except for casting the cookie into a variable and setting its properties. I had to set the properties of the object directly like so:
HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].Expires = expiryDate;
HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].HttpOnly = true;
HttpContext.Current.Response.Cookies["ASP.NET_SessionId"].Value = sessionState.SessionID;
I still cast the cookie into a variable to check if it's null, and return if so.
Related
I have a system where at some point, the user will be locked to a single page. In this situation his account his locked and he cannot be redirected to any other page and this is after authentication.
The verification is done using Page Filters accessing database. To improve performance I have used memory cache.
However, the result wasn't as expected because once the cache is used for a single user it will affect all the others.
As far as i know, you can separate caching using tag helpers per user but I have no idea if this is possible using code
public async Task<IActionResult> Iniciar(int paragemId, string paragem)
{
var registoId = Convert.ToInt32(User.GetRegistoId());
if (await _paragemService.IsParagemOnGoingAsync(registoId))
{
return new JsonResult(new { started = false, message = "Já existe uma paragem a decorrer..." });
}
else
{
await _paragemService.RegistarInicioParagemAsync(paragemId, paragem, registoId);
_registoService.UpdateParagem(new ProducaoRegisto(registoId)
{
IsParado = true
});
await _registoService.SaveChangesAsync();
_cache.Set(CustomCacheEntries.RecordIsParado, true, DateTimeOffset.Now.AddHours(8));
return new JsonResult(new { started = true, message = "Paragem Iniciada." });
}
}
here i only check first if the user account is blocked in the database first without checking cache first and then create the cache entry.
Every user will be locked because of this.
So my point is... Is there a way to achieve this like tag helpers?
The CacheTagHelper is different than cache in general. It works via the request and therefore can vary on things like headers or cookie values. Just using MemoryCache or IDistributedCache directly is low-level; you're just adding values for keys directly, so there's nothing here to "vary" on.
That said, you can compose your key using something like the authenticated user's id, which would then give each user a unique entry in the cache, i.e. something like:
var cacheKey = $"myawesomecachekey-{User.FindFirstValue(ClaimTypes.NameIdentifier)}";
Short of that, you should use session storage, which is automatically unique to the user, because it's per session.
There are several alternatives to the cache. For details please see this link that describes them in greater detail.
Session State
An alternative would be to store the value in session state. This way, the session of one user does not interfere with the ones of others.
However, there are some downsides of this approach. If the session state is kept in memory, you cannot run your application in a server farm because one server does not know of the others session memory. So you would need to save the session state in a cache (REDIS?) or a database.
In addition, as session memory is stored in the server users cannot change it and avoid the redirection that you try to implement. The downside is that this reduces the amount of users that your server can handle because the server needs to have a specific amount of memory per user.
Cookies
You can send a cookie to the client and check for this cookie when the next request arrives at your server. The downside of this approach is that the user can delete the cookie. If the only consequence of a missing cookie is a request to the database, this is neglectable.
You can use session cookies that are discarded by the server when the session expires.
General
Another hint is that you need to clear the state memory when a user signs out so that with the next sign in, the state is correctly set up for the new user.
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 click on refresh button which should restart session:
protected void btnRefresh_Click(object sender, EventArgs e)
{
HttpContext.Current.Session.Abandon();
HttpCookie mycookie = new HttpCookie("ASP.NET_SessionId");
mycookie.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(mycookie);
LblSessionID.Text = HttpContext.Current.Session.SessionID+
" test btnRefresh_Click";
LblIsNewSession.Text = Session.IsNewSession.ToString();
}
But when the button is clicked, the SessionID value in LblSessionID still displays the old value but another label LblIsNewSession will show it as true for IsNewSession. The LblSessionID will then reflect the actual SessionID value when I use asp.net control (like dropdown) that has autopostback="true" and from there SessionID sticks around.
I do use global.asax
Any idea why LblSessionID isn't behaving as it should and is waiting for next postback to start reflecting actual value?
When I launch the web application, the problem is the same - LblSessionID show different value and then change after first postback and stays the same from there.
That's the way it works - If you Abandon the session it won't reflect that until the next Request. It makes sense if you think about it...
Say you have a user that accesses your site and gets a Session ID of 123 (not reflective of an actual value, I know). When you click your button to get a new Session ID, the user's request is from the old Session, and that is the value that is reflected during that Request. Once the session is reset (or abandoned or whatever), the user gets a new Session ID of 321 and subsequent Request's will then reflect that new session ID.
SessionId is not reliable unless you actually store something (anything) in the session.
try
Session.RemoveAll();
Session.Clear();
It is not your code, it is a documented behavior:
"The Abandon method sets a flag in the session state object that indicates that the session state should be abandoned. The flag is examined at the end of the page request. Therefore, the user can still use session objects after you call the Abandon method. As soon as the page processing is completed, the session is removed."
(source: http://support.microsoft.com/kb/899918)
The Abandon() method flags the session collection for clearing at the end of the request, it does not actually clear it immediately.
You can either call the RemoveAll() or Clear() methods for instant deletion of the objects, or issue a Response.Redirect call to the page itself and re-test for the existence of the data.
If the session has expired and the user clicks on a link to another webform, the asp.net authentication automatically redirect the user to the login page.
However, there are cases when the user does not click on links to other webforms. For example: edit link in gridviews, when using AutoCompleteExtender with textboxes and the application attempts to get the information, and basically, in every case when a postback is done and the event is not automatically handled by the asp.net authentication.
What is the best way to handle these exceptions?
UPDATE: I have just modified the question title: forms authentication timeout, instead of the initial session timeout. Thanks for making me aware of this difference.
UPDATE: I have just created a new question with the specific problem I am facing: How to handle exception due to expired authentication ticket using UpdatePanel?. Surprisingly, I have not found much information about it. I would really appreciate your help.
This is why many systems include timers on the page to give approximate timeout times. This is tough with interactive pages. You really need to hook ajax functions and look at the return status code, which is a bit difficult.
One alternative is to use code based on the following which runs early in the page lifecycle and perform an ajax redirect to a login page. Otherwise you are stuck trying to intercept the return code from ajax and in asp.net where the ajax is done 'for you' (ie not a more manual method like jQuery) you lose this ease of detection.
http://www.eggheadcafe.com/tutorials/aspnet/7262426f-3c65-4c90-b49c-106470f1d22a/build-an-aspnet-session-timeout-redirect-control.aspx
for a quick hack you can try it directly in pre_init
http://forums.asp.net/t/1193501.aspx
Edit
what is wanted are for forms auth timeouts, not session timeouts. Forms auth timeouts operate on a different scale than session timeouts. Session timeouts update with every request. Forms auth tickets aren't actually updated until half of the time goes by. So if you have timeouts set to an hour and send in one request 25 minutes into it, the session is reset to an hour timeout, the forms auth ticket isnt touched and expires in 35 minutes! To work around this, sync up the session timeout and the forms auth ticket. This way you can still just check session timeouts. If you don't like this then still - do the below and sync up the timeouts and then parse the auth ticket and read its timeout. You can do that using FormsAuthentication.Decrypt - see:
Read form authentication cookie from asp.net code behind
Note that this code requires that upon login you set some session value - in this case its "UniqueUserId". Also change the login page path below to fit yours.
protected void Application_PreRequestHandlerExecute(object sender, EventArgs e)
{
//Only access session state if it is available
if (Context.Handler is IRequiresSessionState || Context.Handler is IReadOnlySessionState)
{
//If we are authenticated AND we dont have a session here.. redirect to login page.
HttpCookie authenticationCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authenticationCookie != null)
{
FormsAuthenticationTicket authenticationTicket = FormsAuthentication.Decrypt(authenticationCookie.Value);
if (!authenticationTicket.Expired)
{
if (Session["UniqueUserId"] == null)
{
//This means for some reason the session expired before the authentication ticket. Force a login.
FormsAuthentication.SignOut();
Response.Redirect("Login.aspx", true);
return;
}
}
}
}
}
If you're using Forms Authentication, the user will be redirected to the login page when the Forms Authentication ticket expires, which is not the same as the Session expiring.
You could consider increasing the Forms Authentication timeout if appropriate. Even to the extent of using a persistent cookie. But if it does expire, there's no real alternative to redirecting to the login page - anything else would be insecure.
One way to deal with Session timeouts is to use Session as a cache - and persist anything important to a backing store such as a database. Then check before accessing anything in Session and refresh if necessary:
MyType MyObject
{
get
{
MyType myObject = Session["MySessionKey"] as MyType
if (myObject == null)
{
myObject = ... get data from a backing store
Session["MySessionKey"] = myObject;
}
return myObject;
}
set
{
Session["MySessionKey"] = value;
... and persist it to backing store if appropriate
}
}
If you're using a master page or a base page, I would add some logic to one of the events in the page lifecycle to check whether the session is new:
protected void Page_Load(object sender, EventArgs e)
{
if (Session.IsNewSession)
{
//do whatever you need to do
}
}
I have a MasterPage with a combo with languages, the thing is that I would like to assign a default language the moment a user starts the application, after that the user can change between languages. What I understand is that I have to override InitializeCulture method on all of the pages, the problem is, where I can save the selected language? When I use Cache["Culture"] all of the user that starts the application shares the same Cache and overrides the value for all the users logged in.
How can I do that? or how can I save data for a single user's thread when it's not logged in?
Thanks in advance for any help.
use the Session object for data specific to sessions, if you need to persist the choice beyond the session you will need to store it with whatever user data you have
Session["Culture"] = yourculturevar;
If you want to save information locally to a user's computer (as opposed to saving something in a database on the server for logged in users), you can use cookies.
Setting a Cookie
private void SetLanguageCookie(string language)
{
HttpCookie cookie = new HttpCookie("UserSelectedLanguage", language);
// Optionally set expiration for cookie
cookie.Expires = DateTime.Now.AddDays(30);
}
Retrieving a Cookie
private string GetLanguageCookie()
{
HttpCookie cookie = Request.Cookies["UserSelectedLanguage"];
return cookie.Value;
}