Race conditions between touch, set and destroy
Rxthew opened this issue · comments
- I'm submitting a ...
[x ] bug report
[ ] feature request
[x ] question about the decisions made in the repository
[ ] question about how to use this project
- Summary
When implementing logout from passport.js, I get an 'unable to find session to touch' error. I have read and grokked the code in this repository, passport's, as well as express-session's. Apparently, on logout, passport calls save (which becomes 'set' for the purposes of the store), and then it is calling regenerate. Regenerate deletes the session from the store (calling destroy), then when touch is invoked by express-session, the response from the code in connect-mongo throws the error.
If the flow I am understanding is correct, then everything is working as it should (because it is reasonable to expect that there should be no session persisted if you are logged out), therefore being 'unable to find session to touch' is not always an error.
Two questions:
1. Is this flow correct or have I misunderstood something?
2. If it is correct, would it not be better to log or use the debug module for the 'unable to find session to touch' notification
- Other information
I am obtaining these results through API calls via the REST Client extension on VS Code.
Relevant dependencies:
{
"connect-mongo": "^5.0.0",
"express": "~4.16.1",
"express-session": "^1.17.3",
"passport": "^0.6.0",
"passport-local": "^1.0.0"
}
This is the code from the mongostore.ts file which produces the error:
const rawResp = await collection.updateOne(
{ _id: this.computeStorageId(sid) },
{ $set: updateFields },
{ writeConcern: this.options.writeOperationOptions }
)
if (rawResp.matchedCount === 0) {
return callback(new Error('Unable to find the session to touch'))
} else {
this.emit('touch', sid, session)
return callback(null)
}
This is the relevant code form passport.js inside sessionmanager.js file
// clear the user from the session object and save.
// this will ensure that re-using the old session id
// does not have a logged in user
if (req.session[this._key]) {
delete req.session[this._key].user;
}
var prevSession = req.session;
req.session.save(function(err) {
if (err) {
return cb(err)
}
// regenerate the session, which is good practice to help
// guard against forms of session fixation
req.session.regenerate(function(err) {
if (err) {
return cb(err);
}
if (options.keepSessionInfo) {
merge(req.session, prevSession);
}
cb();
});
});
}
This is the code from express-session inside store.js (inherited by connect-mongo's store) which defines the regenerate function:
Store.prototype.regenerate = function(req, fn){
var self = this;
this.destroy(req.sessionID, function(err){
self.generate(req);
fn(err);
});
};
The code inside session.js from express-session defines save shows the 'set' method is being invoked in the Mongostore.
defineMethod(Session.prototype, 'save', function save(fn) {
this.req.sessionStore.set(this.id, this, fn || function(){});
return this;
});
As you might know, express-session generates a 'session' middleware which generally runs before route handlers, however - and this is the bit I'm haziest on - after logout invokes res.redirect
or res.status().json()
, the response's end function is called. The session middleware rewrites end so that when it is called touch is invoked.
See index.js in express-session, the part I am referring to starts here:
// proxy end() to commit the session
var _end = res.end;
var _write = res.write;
var ended = false;
res.end = function end(chunk, encoding) {
if (ended) {
return false;
}
ended = true;
The more relevant bit:
} else if (storeImplementsTouch && shouldTouch(req)) {
// store implements touch method
debug('touching');
store.touch(req.sessionID, req.session, function ontouch(err) {
if (err) {
defer(next, err);
}
debug('touched');
writeend();
});
As an aside I'm finding it suspicious that no else is experiencing this issue, especially since the change in passport's logout functionality has been in place since May 2022. Eliminating that code nullified the error, but this then persisted sessions that should have been deleted when the user logs out which makes little sense, so if there is anything I have failed to grasp, please let me know.
I have changed the title of this issue and redacted the inaccuracies in my original post. After debugging this further, I realised that the error was only appearing at certain sequences and then not consistently. The problem is that the touch method is not meant to be called before destroy, but due to a race condition that is what ends up happening. I am not quite sure but this seems to be an issue with at the express-session level rather than connect-mongo.
My hack to prevent this error involves manipulating the shouldTouch
function in the express session index.js file. In that function, touch is not invoked if the session ID is not a string, so in my logout controller I change the session ID before logging out. If logout fails for some reason, this is inconsequential since the session ID is derived from the cookie on each request.