fix(auth): a Google connect names its tab's account too, and a refused tab says why

- /auth/google/connect is a navigation and carries no headers: the tab names the account it
  was built for in `expect`, checked by the same comparison as the request header
  (built_for_another_account); a mismatch lands on /?account=changed
- the link marker names the OAuth state of its own flow: an older flow coming back no longer
  spends the marker of the connect still open (which then returned as a plain sign-in)
- a 401 account_changed reloads the tab with a one-time flag, and the reloaded tab says it
  now shows the account signed in
- a refused switch to a suspended or not-yet-approved account says so in the signed-in
  app's words, naming the address a request was recorded for
- the prefs merge locks FOR NO KEY UPDATE, so a transaction that only added a row referring
  to the account does not hold the save up; a test pins the lock
- giving up a lost page closes a channel page open over it
- e2e README names the chromium-only specs
This commit is contained in:
2026-10-21 02:05:33 +02:00
parent 94418a087e
commit ef99f7b5c7
19 changed files with 487 additions and 121 deletions
+15 -7
View File
@@ -272,15 +272,23 @@ def stamp_session(request: Request, user: User) -> None:
EXPECTED_ACCOUNT_HEADER = "x-siftlode-expect-account"
def built_for_another_account(user_id: int, expected: str | None) -> bool:
"""Whether a tab naming `expected` is showing another account than the one signed in. No name is
no claim (a caller from before this check, a script): only a mismatch is refused, and the
session stays — it is the other tab's sign-in, and it holds. The one comparison for every way a
tab names its account: this header on a request, `expect` on a navigation."""
if expected is None or expected == str(user_id):
return False
log.info(
"a request from a tab built for another account was refused",
extra=logs.event("auth.session.account_changed", user_id=user_id),
)
return True
def current_user(request: Request, db: Session = Depends(get_db)) -> User:
user = _session_user(request, db, session_user_id(request))
expected = request.headers.get(EXPECTED_ACCOUNT_HEADER)
if expected is not None and expected != str(user.id):
# The session stays: it is the other tab's sign-in, and it holds.
log.info(
"a request from a tab built for another account was refused",
extra=logs.event("auth.session.account_changed", user_id=user.id),
)
if built_for_another_account(user.id, request.headers.get(EXPECTED_ACCOUNT_HEADER)):
raise refused(401, "account_changed")
return user