From 69dab023dec99b0859fdf544a898cc9b0c63a647 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 23 Feb 2026 14:57:53 +0000 Subject: [PATCH] Account authorize passes account_did, login/logout signal via Redis - OAuth authorize: pass account_did (g.device_id) in both success and error redirects so client apps can track the device - Magic link login: set did_auth:{device_id} in Redis so client apps detect login even when their prompt=none cache says "no" - Logout + SSO-logout: clear did_auth:{device_id} from Redis Co-Authored-By: Claude Opus 4.6 --- bp/auth/routes.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/bp/auth/routes.py b/bp/auth/routes.py index ee40a33..ce04927 100644 --- a/bp/auth/routes.py +++ b/bp/auth/routes.py @@ -67,12 +67,18 @@ def register(url_prefix="/auth"): if redirect_uri != expected_redirect: return "Invalid redirect_uri", 400 + # Account's own device id — always available via factory hook + account_did = g.device_id + # Not logged in if not g.get("user"): if prompt == "none": - # Silent check — no interactive login, return error + # Silent check — pass account_did so client can watch for future logins sep = "&" if "?" in redirect_uri else "?" - return redirect(f"{redirect_uri}{sep}error=login_required&state={state}") + return redirect( + f"{redirect_uri}{sep}error=login_required" + f"&state={state}&account_did={account_did}" + ) authorize_path = request.full_path store_login_redirect_target() return redirect(url_for("auth.login_form", next=authorize_path)) @@ -110,7 +116,10 @@ def register(url_prefix="/auth"): s.add(oauth_code) sep = "&" if "?" in redirect_uri else "?" - return redirect(f"{redirect_uri}{sep}code={code}&state={state}") + return redirect( + f"{redirect_uri}{sep}code={code}&state={state}" + f"&account_did={account_did}" + ) # --- Grant verification (internal endpoint) ------------------------------ @@ -276,6 +285,20 @@ def register(url_prefix="/auth"): # Fresh account session ID for grant tracking qsession[ACCOUNT_SESSION_KEY] = secrets.token_urlsafe(32) + # Signal login for this device so client apps can detect it + try: + from shared.browser.app.redis_cacher import get_redis + import time as _time + _redis = get_redis() + if _redis: + await _redis.set( + f"did_auth:{g.device_id}", + str(_time.time()).encode(), + ex=30 * 24 * 3600, + ) + except Exception: + current_app.logger.exception("[auth] failed to set did_auth in Redis") + redirect_url = pop_login_redirect_target() return redirect(redirect_url, 303) @@ -296,6 +319,15 @@ def register(url_prefix="/auth"): except SQLAlchemyError: current_app.logger.exception("[auth] failed to revoke grants") + # Clear login signal for this device + try: + from shared.browser.app.redis_cacher import get_redis + _redis = get_redis() + if _redis: + await _redis.delete(f"did_auth:{g.device_id}") + except Exception: + pass + qsession.pop(SESSION_USER_KEY, None) qsession.pop(ACCOUNT_SESSION_KEY, None) from shared.infrastructure.urls import blog_url @@ -318,6 +350,15 @@ def register(url_prefix="/auth"): except SQLAlchemyError: current_app.logger.exception("[auth] failed to revoke grants") + # Clear login signal for this device + try: + from shared.browser.app.redis_cacher import get_redis + _redis = get_redis() + if _redis: + await _redis.delete(f"did_auth:{g.device_id}") + except Exception: + pass + qsession.pop(SESSION_USER_KEY, None) qsession.pop(ACCOUNT_SESSION_KEY, None) from shared.infrastructure.urls import blog_url