From 186c0d581b34734c8c7e85894623800f088a1e33 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 13:41:01 +0000 Subject: [PATCH] Add prompt=none to OAuth authorize, remove propagation chain Account's authorize endpoint now supports prompt=none: returns error=login_required redirect when user isn't logged in instead of bouncing to interactive login. Removed /propagate endpoint since client apps now detect auth state via prompt=none handshake. Co-Authored-By: Claude Opus 4.6 --- bp/auth/routes.py | 53 +++++++---------------------------------------- 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/bp/auth/routes.py b/bp/auth/routes.py index be85008..ee40a33 100644 --- a/bp/auth/routes.py +++ b/bp/auth/routes.py @@ -58,6 +58,7 @@ def register(url_prefix="/auth"): redirect_uri = request.args.get("redirect_uri", "") state = request.args.get("state", "") device_id = request.args.get("device_id", "") + prompt = request.args.get("prompt", "") if client_id not in ALLOWED_CLIENTS: return "Invalid client_id", 400 @@ -66,8 +67,12 @@ def register(url_prefix="/auth"): if redirect_uri != expected_redirect: return "Invalid redirect_uri", 400 - # Not logged in — bounce to magic link login, then back here + # Not logged in if not g.get("user"): + if prompt == "none": + # Silent check — no interactive login, return error + sep = "&" if "?" in redirect_uri else "?" + return redirect(f"{redirect_uri}{sep}error=login_required&state={state}") authorize_path = request.full_path store_login_redirect_target() return redirect(url_for("auth.login_form", next=authorize_path)) @@ -271,52 +276,8 @@ 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() - 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) + return redirect(redirect_url, 303) @auth_bp.post("/logout/") async def logout():