Feature-Flag Rollout vs. Immediate OAuth2 Cutover for a Zero-Downtime Passport Migration
0 reputation · 07 Mar 2021, 19:53 UTC
0 reputation · 07 Mar 2021, 19:53 UTC
We run a small Express application that authenticates users with Passport's local strategy backed by server-side sessions (connect-mongo). The plan is to move to an OAuth2 authorization-code flow without forcing any user to log out mid-migration, and I need to commit to one of two documented approaches before writing code.
Approach A is incremental: keep the local strategy active, add a feature-flag middleware that routes a growing percentage of traffic through the new OAuth2 guard, and share the existing session store between both guards. Approach B is an immediate cutover: issue tokens on first login, deprecate password auth once every active user has completed the flow, and keep refresh-token rotation disabled to limit the attack surface.
The unresolved decision is whether refresh-token rotation should be enabled during the overlap. Rotation improves safety if a credential leaks mid-migration, but it complicates the shared-session design and risks invalidating tokens for users still on the old path. We are on Passport v0.6.x and have not verified behavior against v1.x middleware signatures.
35275 reputation · 08 Mar 2021, 01:19 UTC
Take Approach A (feature-flag rollout), but keep refresh-token rotation disabled during the overlap, and add a single custom serializeUser/deserializeUser pair that normalizes the user shape for both strategies. That combination answers all three of your questions: it avoids the rotation race, it carries less risk than an immediate cutover even for a small user base, and it is the only reliable way to keep two Passport strategies mounted against one session store.
Yes, this is a real risk, not a theoretical one — though the exact failure mode depends on your token storage design, so treat this as the likely explanation rather than a confirmed fact for your codebase. The problem: with a percentage-based flag, the same user can hit the OAuth2 guard on one request (rotating the refresh token and writing the new token) and the local guard on the next request (which knows nothing about the rotation). If the old guard re-saves the session from a stale copy, or a second tab replays the pre-rotation refresh token, you get either a token-mismatch rejection or a session write conflict. These surface as intermittent forced logouts or 401/500s — exactly the mid-migration disruption you are trying to avoid.
Rotation's benefit (containing a leaked refresh token) matters most when tokens are long-lived and widely exposed. During a short overlap window with a small user base, the race risk outweighs the leak-containment benefit. Enable rotation after the flag reaches 100% and the local strategy is removed.
For a small user base the absolute numbers are small either way, but the risk profile differs in kind. Approach B's real weakness is not rotation being off — it is that "deprecate password auth once every active user has completed the flow" has no clean boundary. Long-tail users who return after cutover hit a dead end unless you keep a fallback path alive anyway — at which point you have reinvented Approach A, but untested and under time pressure. A percentage flag gives you a kill switch (set it back to 0%) that a cutover does not. For a small app, that reversibility is worth more than the simplicity of B.
Passport has exactly one serializeUser per app instance, so both strategies must agree on what goes into the session. The documented pattern is to serialize only a stable identifier and re-load on deserialize:
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser(async (id, done) => {
try { done(null, await User.findById(id)); } catch (e) { done(e); }
});Make sure your OAuth2 verify callback resolves the provider identity to the same user document the local strategy uses, so user.id is identical on both paths. If the OAuth2 callback currently passes the token profile through as the user object, that is the conflict to fix first.
On the flag itself: decide the routing before authentication runs, and make the assignment sticky per user (e.g., hash of user ID, or a flag stored on the user/session) rather than a per-request random roll. A random per-request roll is what creates the toggling that triggers the race in question 1.
logout() behavior differs from earlier 0.x releases; do not assume v1.x signatures without checking the changelog for your exact version.One detail that could change this recommendation: where you plan to store refresh tokens (session store vs. a separate token collection). If they live outside the session entirely, the rotation race shrinks considerably and enabling rotation during overlap becomes defensible.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.