From 17581a7b75b6d1ecf3678805b6400e1e6a09dba4 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 13:28:03 +0000 Subject: [PATCH] Re-add propagation chain for initial login Device cookies handle subsequent auth changes (logout/re-login), but the initial login needs the chain to create grants on each app and link them to device cookies. Dead apps skipped via health check. Co-Authored-By: Claude Opus 4.6 --- bp/auth/routes.py | 46 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/bp/auth/routes.py b/bp/auth/routes.py index 443e13e..be85008 100644 --- a/bp/auth/routes.py +++ b/bp/auth/routes.py @@ -271,8 +271,52 @@ 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 create grants + and set device cookies. Dead apps are skipped.""" + import os, aiohttp + from urllib.parse import quote + + chain = qsession.get("sso_chain", []) + final = qsession.get("sso_final", account_url("/")) + + if not g.get("user"): + qsession.pop("sso_chain", None) + qsession.pop("sso_final", None) + return redirect(final) + + comeback = account_url("/auth/propagate") + + while chain: + next_app = chain.pop(0) + qsession["sso_chain"] = chain + + internal = (os.getenv(f"INTERNAL_URL_{next_app.upper()}") or f"http://{next_app}:8000").rstrip("/") + try: + async with aiohttp.ClientSession() as http: + async with http.head( + internal, + timeout=aiohttp.ClientTimeout(total=2), + allow_redirects=True, + ) as resp: + if resp.status < 500: + login = app_url(next_app, f"/auth/login?next={quote(comeback, safe='')}") + return redirect(login) + except Exception: + current_app.logger.warning("[propagate] skipping dead app: %s", next_app) + continue + + qsession.pop("sso_chain", None) + qsession.pop("sso_final", None) + return redirect(final) @auth_bp.post("/logout/") async def logout():