From 9845c8d0f77fa3e23037cc4c1960d1f4e343fe29 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Tue, 18 Aug 2026 00:22:15 +0200 Subject: [PATCH] fix(auth): epoch-check the target on account switch to avoid a full wallet logout (B-core-9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST /api/me/switch validated wallet membership, access and suspension but not the target account's session epoch. Switching to an account revoked elsewhere (a password reset/change or "log out everywhere" bumped its epoch) made it the session default, so the next request's current_user hit the epoch mismatch on the default identity and cleared the WHOLE wallet session — logging out every account in the browser. Mirror the suspend guard: drop the stale account from the switch list and return 403 instead of switching. --- backend/app/routes/me.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/app/routes/me.py b/backend/app/routes/me.py index 5d683d5c..458e2473 100644 --- a/backend/app/routes/me.py +++ b/backend/app/routes/me.py @@ -72,6 +72,18 @@ def switch_account( # Don't make a suspended account the active one — the next request's current_user would # see the suspension and clear the WHOLE wallet session, logging out every account here. raise HTTPException(status_code=403, detail="That account is suspended.") + # Same reasoning for a REVOKED session (a password reset/change or "log out everywhere" bumped the + # account's epoch): making it the session default would trip current_user's epoch check on the next + # request and clear the whole wallet. Mirror the suspend guard — drop the stale account from the + # switch list and refuse, rather than logging everyone out. (B-core-9) + epochs = request.session.get("epochs") or {} + if int(epochs.get(str(target), 0)) != (u.session_epoch or 0): + request.session["account_ids"] = [ + a for a in (request.session.get("account_ids") or []) if a != target + ] + raise HTTPException( + status_code=403, detail="That account was signed out elsewhere — sign in again." + ) request.session["user_id"] = target return {"ok": True, "user_id": target}