Uncaught serializeUser exception and session growth in Passport session handling
0 reputation · 10 Feb 2020, 02:24 UTC
0 reputation · 10 Feb 2020, 02:24 UTC
Passport session management relies on serializeUser and deserializeUser to store and reconstruct user identity across requests. Research notes indicate potential retention of session data when serializeUser throws an uncaught exception, with behavior varying by Passport version and session store implementation.
The goal is to clarify the documented cleanup guarantees for session objects under serialization failures, and how the internal request.user property is handled when deserializeUser throws synchronously. Version assumptions for Passport 0.4.x, 0.5.x and 0.6.x are relevant, as is the distinction between in-memory and external stores.
Does Passport guarantee removal of the partially created session entry when serializeUser fails? Is request.user cleared when deserializeUser throws synchronously during request processing? How does session regeneration behave when authentication fails mid-process across these versions?
35275 reputation · 10 Feb 2020, 08:20 UTC
No — Passport does not document or implement a guarantee that a partially created session entry is removed when serializeUser fails. Cleanup is entirely a function of express-session and your store, not Passport. And when deserializeUser errors, Passport routes the error to next(err) and simply never populates req.user for that request; it does not actively clear anything.
During req.login(), Passport's SessionManager calls your serializeUser. If it yields an error, Passport passes it to next(err) and stops. The key point is the write path: express-session only persists the session to the store when the response ends. Because the failed login short-circuits before req.session.passport.user is committed, a failed login typically leaves no new persisted entry.
The nuance — and the likely source of observed session growth — is saveUninitialized: true. With that option, the pre-existing anonymous session created for the request is still saved at response end, error or not. Combined with a store that has no TTL (MemoryStore, or Redis without expiry), failed login attempts accumulate empty sessions. That is an express-session/store behavior, not a serializeUser leak. So the fix for growth is usually:
saveUninitialized: false (and review resave).ttl in connect-redis).Two distinct cases matter:
done(err)): Passport catches this internally, calls next(err), and req.user is left unset for that request. Your error-handling middleware runs normally.passport.deserializeUser(async (id, done) => {
try {
const user = await loadUser(id);
done(null, user || false);
} catch (err) {
done(err);
}
});In Passport 0.4.x and 0.5.x, login mutates the existing session in place. In 0.6.x, req.login() explicitly regenerates the session (session-fixation mitigation), which changes when passport data is written relative to the old session ID. If authentication fails mid-process under 0.6.x, the regeneration timing means the old session may already be flagged for replacement — behavior you should verify against your installed version rather than assume. Check with:
npm ls passport express-sessionand read node_modules/passport/lib/sessionmanager.js for the exact error path in your version.
serializeUser in a test route, then inspect the store (redis-cli KEYS '*' or MemoryStore contents) after the failed request to confirm whether an entry persisted.DEBUG=express-session to see whether a save occurs on failed-login responses.deserializeUser throws synchronously and assert the process survives and error middleware sees the request.Uncertainty note: Passport's public docs do not formally specify cleanup guarantees on serialization failure; treat the above as observed framework/store behavior and verify against your exact versions.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.