Add prompt=none to OAuth authorize, remove propagation chain
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 53s

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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-23 13:41:01 +00:00
parent 17581a7b75
commit 186c0d581b

View File

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