From 6275049025602d4240cd988db4a0fee11b4c8c72 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 12:40:08 +0000 Subject: [PATCH] Propagate login to all client apps via OAuth chain After magic link login, account bounces through each client app's /auth/login to establish local sessions via OAuth. Each app does its OAuth flow (instant since account is logged in) then redirects back to /auth/propagate for the next app in the chain. Co-Authored-By: Claude Opus 4.6 --- bp/auth/routes.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/bp/auth/routes.py b/bp/auth/routes.py index 94afc82..09b3266 100644 --- a/bp/auth/routes.py +++ b/bp/auth/routes.py @@ -230,8 +230,34 @@ def register(url_prefix="/auth"): # Fresh account session ID for grant tracking qsession[ACCOUNT_SESSION_KEY] = secrets.token_urlsafe(32) + # Propagate login to all client apps via OAuth chain redirect_url = pop_login_redirect_target() - return redirect(redirect_url, 303) + qsession["sso_final"] = redirect_url + qsession["sso_chain"] = list(ALLOWED_CLIENTS) + return redirect(url_for("auth.propagate"), 303) + + @auth_bp.get("/propagate") + @auth_bp.get("/propagate/") + async def propagate(): + """Chain through each client app's OAuth login to propagate the + account session. Each app does its OAuth flow (instant since + account is already logged in) then redirects back here. When + the chain is empty, redirect to the original target.""" + chain = qsession.get("sso_chain", []) + final = qsession.get("sso_final", account_url("/")) + + if not chain or not g.get("user"): + qsession.pop("sso_chain", None) + qsession.pop("sso_final", None) + return redirect(final) + + next_app = chain.pop(0) + qsession["sso_chain"] = chain + + from urllib.parse import quote + comeback = account_url("/auth/propagate") + login_url = app_url(next_app, f"/auth/login?next={quote(comeback, safe='')}") + return redirect(login_url) @auth_bp.post("/logout/") async def logout():