-
-
Notifications
You must be signed in to change notification settings - Fork 987
Only touch sessions in the store when rolling sessions are enabled #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Only touch sessions in the store when rolling sessions are enabled #531
Conversation
- Currently, disabling rolling sessions suppresses unmodified cookies being sent back to the client (line 447). However, the session is "touched" in the store regardless of whether rolling sessions are enabled (line 339). - This results in the expiry time in the cookie and in the persistent session deviating. Specifically, the expiry of the persistent session is pushed back on each and every request whereas the cookie expiry is only updated if the request changes the session. - This prevents the use case of a "session ping" request that is sent by the client to test if their session is still valid. We have achieved this by turning off rolling sessions and placing the handling of the ping early in the express stack. A stack entry is then placed after the ping handling to make a benign change to the session to cause it to be stored, in effect, touching the session much like rolling sessions. An example router might look like (using passport with disabled rolling sessions): router.get('/ping', function (req, res, next) { if (!req.isAuthenticated()) { res.sendStatus(403); } else { res.sendStatus(200); } }); router.use(function (req, res, next) { // Push back session expiry and make benign change to // force persistent storage. req.session.cookie.maxAge = 3600; req.session._dummy = req.session._dummy ? (req.session._dummy + 1) : 0; next(); }); - This appears to work from a client perspective as the cookie expires at the appropriate time. However, the session expiry in the store is always pushed back, even for ping requests. This means that from a server perspective the session is still valid for some period after the cookie expiry. The store expiry therefore cannot be trusted or used to identify expired sessions in the store for purging or clean up. - There is also a security issue here since the server has sessions in the store which appear to be valid but should have expired. This means that a malicious user could potentially resurrect their cookie and the server would accept requests received before the expiry in the store.
Hi and thanks for your PR! Many users have different expirations for the server and the client and this would be a breaking change. We can either (a) hold this PR until a new major version is released of (b) the behavior can be opt-in such that current users do not get this new behavior. Also, please add tests which test the new behavior. They should fail without your patch and pass with your patch. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs tests to bring the coverage back to 100%.
I see you got the coverage back up, but can you still (a) let me know which option you are going to choose and (b) add test(s) for your change? |
Hi Doug. I'm fine with either option a) or b). The purist in me thinks it should be the default behaviour as I think the expiry times in the cookie and the store should match at all times. But the pragmatist in me concedes that it might break others so perhaps an option would be a better way to go. I'll get started on adding some new tests for it. Thanks, |
Right, I haven't looked into it close enough, but on the surface it does feel like it should be the default behavior, though the change itself breaks tests which is definitely not a good sign for what will happen to existing apps using 1.x. We can of course land in 2.x but also can put as opt-in in 1.x and switch the default behavior in 2.x (or even remove the switching all together and it's the only behavior) which would let folks get ready right away. |
…ys be touched - this option was added to support the current behaviour where unmodified sessions are always touched in the store regardless of whether rolling sessions are enabled - it is now possible to avoid having unmodified sessions being touched when rolling sessions are configured by setting this option to false
Hi Doug, I have added a new option alwaysTouchUnmodified so that the current behaviour is maintained. Not sure about the option name, so happy to change it if you have something better. |
Awesome! Thr name seems fine to me. I assume that since this is opt-in, the changes made to the existing tests can be reverted now, right? |
Ah yes, good pick up. I'll revert those changes now. |
Hi Doug, I have reverted my changes to the existing tests now that the existing behaviour is the default. Let me know if there is anything else that you would like done. Thanks, |
The changes look good, but there is some kind of flaky test included:
That will fail sometimes and pass other times. Not sure what the issue is yet, but I'm looking into it to see if there is a change to be made. Would need to be fixed in order to merge. I haven't seen it happen on Travis CI but it happens around 80% on all three of my machines. Does it happen on your machine? @gabeio can you pull this PR and run the tests a few times to see if you also see the test failure? |
index.js
Outdated
@@ -336,7 +344,7 @@ function session(options) { | |||
}); | |||
|
|||
return writetop(); | |||
} else if (storeImplementsTouch && shouldTouch(req)) { | |||
} else if ((alwaysTouchUnmodified || rollingSessions) && storeImplementsTouch && shouldTouch(req)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this extra logic be inside the shouldTouch
function? Or if not, why not? I would expect that function is all that would be called to determine if a touch occurs, and this is controlling the touching.
@@ -127,6 +131,10 @@ function session(options) { | |||
saveUninitializedSession = true; | |||
} | |||
|
|||
if (alwaysTouchUnmodified === undefined) { | |||
alwaysTouchUnmodified = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forget looking through the comments: is there an intention to make alwaysTouchUnmodifed
the default behavior in the next major? I ask because all default options that are Booleans should be false
such that undefined
and null
are stand in values. The only options that default to true
are deprecated options, but this doesn't have a deprecation warning on it.
Regarding the flakey test, I can see how it might fail on a fast machine since there is no delay between capturing the first expiry time and executing the second request. So there is a chance that the expiry times are equal due to the millisecond resolution. I'm running on a VM which appears to be slow enough that it never fails. I will put a small delay in there to ensure the expiry times resulting from the two requests should always differ. I'll move the logic into shouldTouch(). I hadn't appreciated that this was the only place shouldTouch() was called and wanted to limit the impact to just this location. I'll look into renaming the option so I can change the default value to false. Thanks David |
- also fixed up intermittent test failures that were dependent on at least 1 msec elapsing between expiry time comparisons
Hi Doug, Regarding the default for this Boolean option being If you agree with this plan, does that mean the option should have a similar deprecation warning to the other Booleans defaulting to If you don't think the default behaviour should change in the next major, then I will rename the option so I can flip the default value to Hopefully the intermittent test failures should be fixed and I've moved the logic into shouldTouch(). Thanks, |
Hi @dtymon I have never had any issue with the existing behavior, and it seems no one else has mentioned this in all these years of very heavy use. Without a larger set of opinions on this, I'm not sure we can say the current behavior is unexpected, seeing as no one has seemed to stumble into whatever the issue is here. I, personally, expect the existing behavior as I mentioned in my first repose :) That means at this point we have 1 in favor and 1 not. Not only is that a tie, but no where near enough opinions to really say one way or the other. We can leave this PR open for a few months to collect additional opinions 👍 |
Oh well, I guess we will have to agree to disagree on this one. I would have thought that the session expiry time should be essentially identical on client and server regardless of how the middleware is configured but perhaps others think differently. |
Why not just not update the session expiration when it's touched on the server? The touch just indicated the session was accessed, and the underlying store chooses what it should do with that information. It can certainly decide to update the expiration on the server, but it can also decide not to do that. |
The entire reason this module started calling |
I'm using the MySQL store that updates the 'expires' column when touch() is called. It looks like the memcached store doesn't store an explicit session expiry, only the cookie and session object, and so does not suffer the issue. But the MySQL store has an explicit 'expires' column which is updated in the touch() implementation. |
Gotcha. I mean, it sounds like you should just add an option to the store module to allow you to choose if you want session access ("touch") to keep bumping that column or not, right? That sounds like a feature request to the store module. |
(I'm just trying to think about this, is all. I keep reading and re-reading your initial post trying to understand the issue and I think I do, but not sure. It would have been nice to get an issue filed that explains what you're running into with full examples I can see myself to better understand the problem at hand here -- I'm not trying to be mean or anything, just hard for me to reason about without grasping the issue at hand and instead I just have a solution presented. I hope that makes sense). |
Yeah, it does seem to be a MySQL store issue. I hadn't appreciated that it was up to the store to decide what it wanted to do with a touch(). Instead I thought it was in line with the UNIX command |
I will try to explain what we are trying to achieve to hopefully better explain how we got to this point.
Hopefully this might give a better understanding on how we got to this point. |
Wouldn't this break if the user's machine was offline during this time? Or does the failure to reach the server cause an immediate log out? |
Yes. Failure to reach the server to do the ping will cause the app to log out the user. Due to the nature of the app, it is imperative that it is not abused and so anything other than a successful ping will result in the app shutting down. |
Typically this is the common use-case for the expirrations not matching on the server and the client (usually the server will have a shorter expiration), though of course a user can just disappear and come back a few days later on your 1 hour session. This concern is unfortinatley completely outside of this module and would be something up to the store. I did store session in MySQL before just like you and this is actually a problem that is hard to solve when the underlying storage itself doesn't eject (vs something like Redis). |
OK, cool. I'm thinking we will just write our own MySQL store as it is not very difficult and that way we will be able to implement whatever functionality we require. |
But yea, I get what you're saying. It does indeed sound like you are describing an issue with your store, though. |
Thanks Doug, I agree it is a store issue. Perhaps we can reject this pull request then. |
I mean, I'm still OK with adding the functionality; the tangent was just a discussion that stemmed from changing the default behavior, rather than just adding yet another optional behavior users can opt-in to. Usually for the ping case, I think most of the time, the ping is used to specifically case the session not to expire (for example, because the user is still on the site / page, so you don't want it to suddenly log them out). I haven't really heard the case to do the opposite case before (like you're doing), especially because I would certainly be miffed if my router rebooted and got kicked out of all the sites I was logged in to :) I'm not saying your use-case is not valid, but I think I understand it as the opposite of what I usually always see, which may also be why this has never come up before on here, for example. |
Haha, that reminds me of all the times I've had really crappy WiFI at hotels and on air planes, when the connection would just be down for a few minutes every so often and you have to sit there waiting for it to come back to continue working. Also reminds me of back in the day where a common "security" mechanism was to pin your login session to a single IP and when you're at a school or similar that redirected traffic though multiple outbound IPs and you constantly keep getting booted out of web site because the IP that site would see keeps changing @_@ |
So I ended up here because my store (connect-session-sequelize) is writing to the db every request, regardless of whether rolling is on or off in express session. I thought the touch on the store was to be dumb, and just do an update when requested. If I understand what you guys are saying, you want touch to just be a communication to the store that the session was accessed? So essentially a store can break the functionality of the "rolling" option? |
In looking into this I basically decided the way forward for me was to disable the touch() in my store, so that I could control when the db is accessed. Asking to store to control timing of touch() is hard because the current expiration of the session is not passed to the touch() method so it would have to do a db read before it could decide not to update the db. The expiration of the cookie is passed in some configs, but there is no guarantee that that will match the store value. |
Hey Doug, This PR still seems to be important. The store has no way of knowing if the session has changed. Right now, if a session changes, |
9d2e29b
to
408229e
Compare
Currently, disabling rolling sessions suppresses unmodified
cookies being sent back to the client (line 447). However, the
session is "touched" in the store regardless of whether rolling
sessions are enabled (line 339).
This results in the expiry time in the cookie and in the
persistent session deviating. Specifically, the expiry of the
persistent session is pushed back on each and every request
whereas the cookie expiry is only updated if the request
changes the session.
This prevents the use case of a "session ping" request that is
sent by the client to test if their session is still valid. We
have achieved this by turning off rolling sessions and placing
the handling of the ping early in the express stack. A stack
entry is then placed after the ping handling to make a benign
change to the session to cause it to be stored, in effect,
touching the session much like rolling sessions. An example
router might look like (using passport with disabled rolling
sessions):
expires at the appropriate time. However, the session expiry
in the store is always pushed back, even for ping requests.
This means that from a server perspective the session is
still valid for some period after the cookie expiry. The
store expiry therefore cannot be trusted or used to identify
expired sessions in the store for purging or clean up.