From a93a456ac56e862f8a66d39660a331d1eb326d87 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 12:17:22 +0000 Subject: [PATCH] Remove sso_hint cookie, add sso-clear logout chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sso_hint on .rose-ash.com was blocked by Safari ITP — the exact problem we're solving. Replaced with redirect chain: account logout chains through each client app's /auth/sso-clear to clear all first-party sessions without any cross-domain cookies. Co-Authored-By: Claude Opus 4.6 --- infrastructure/factory.py | 34 ---------------------------------- infrastructure/oauth.py | 13 ++++++++++--- 2 files changed, 10 insertions(+), 37 deletions(-) diff --git a/infrastructure/factory.py b/infrastructure/factory.py index 7ba8718..ffac1e7 100644 --- a/infrastructure/factory.py +++ b/infrastructure/factory.py @@ -123,40 +123,6 @@ def create_base_app( for fn in before_request_fns: app.before_request(fn) - # Silent SSO: if account set sso_hint cookie, trigger OAuth once - if name != "account": - from urllib.parse import quote as _quote - - @app.before_request - async def _sso_check(): - from quart import session as qs - if request.path.startswith("/auth/"): - return - - uid = qs.get("uid") - has_hint = request.cookies.get("sso_hint") - - # SSO revoked (account logged out) → clear local session - if uid and not has_hint: - qs.pop("uid", None) - qs.pop("cart_sid", None) - qs.pop("sso_checked", None) - return - - # Already logged in locally - if uid: - return - - # No hint → nothing to do - if not has_hint: - return - - # Has hint but no local session → trigger silent OAuth once - if qs.get("sso_checked"): - return - qs["sso_checked"] = True - return redirect(f"/auth/login/?next={_quote(request.url, safe='')}") - @app.before_request async def _csrf_protect(): await protect() diff --git a/infrastructure/oauth.py b/infrastructure/oauth.py index 9c71908..61bdb14 100644 --- a/infrastructure/oauth.py +++ b/infrastructure/oauth.py @@ -127,16 +127,23 @@ def create_oauth_blueprint(app_name: str) -> Blueprint: qsession.clear() resp = redirect("/") resp.delete_cookie("blog_session", domain=".rose-ash.com", path="/") - resp.delete_cookie("sso_hint", domain=".rose-ash.com", path="/") return resp + @bp.get("/sso-clear") + @bp.get("/sso-clear/") + async def sso_clear(): + """Clear local session, then redirect to next app in logout chain.""" + qsession.pop(SESSION_USER_KEY, None) + qsession.pop("cart_sid", None) + next_url = request.args.get("next", "/") + return redirect(next_url) + @bp.post("/logout") @bp.post("/logout/") async def logout(): qsession.pop(SESSION_USER_KEY, None) qsession.pop("cart_sid", None) - qsession.pop("sso_checked", None) - # Redirect through account to clear the SSO session too + # Redirect through account to clear all app sessions return redirect(account_url("/auth/sso-logout/")) return bp