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():