Account authorize passes account_did, login/logout signal via Redis
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 48s

- 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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-23 14:57:53 +00:00
parent 186c0d581b
commit 69dab023de

View File

@@ -67,12 +67,18 @@ def register(url_prefix="/auth"):
if redirect_uri != expected_redirect: if redirect_uri != expected_redirect:
return "Invalid redirect_uri", 400 return "Invalid redirect_uri", 400
# Account's own device id — always available via factory hook
account_did = g.device_id
# Not logged in # Not logged in
if not g.get("user"): if not g.get("user"):
if prompt == "none": 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 "?" 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 authorize_path = request.full_path
store_login_redirect_target() store_login_redirect_target()
return redirect(url_for("auth.login_form", next=authorize_path)) return redirect(url_for("auth.login_form", next=authorize_path))
@@ -110,7 +116,10 @@ def register(url_prefix="/auth"):
s.add(oauth_code) s.add(oauth_code)
sep = "&" if "?" in redirect_uri else "?" 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) ------------------------------ # --- Grant verification (internal endpoint) ------------------------------
@@ -276,6 +285,20 @@ def register(url_prefix="/auth"):
# Fresh account session ID for grant tracking # Fresh account session ID for grant tracking
qsession[ACCOUNT_SESSION_KEY] = secrets.token_urlsafe(32) 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() redirect_url = pop_login_redirect_target()
return redirect(redirect_url, 303) return redirect(redirect_url, 303)
@@ -296,6 +319,15 @@ def register(url_prefix="/auth"):
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("[auth] failed to revoke grants") 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(SESSION_USER_KEY, None)
qsession.pop(ACCOUNT_SESSION_KEY, None) qsession.pop(ACCOUNT_SESSION_KEY, None)
from shared.infrastructure.urls import blog_url from shared.infrastructure.urls import blog_url
@@ -318,6 +350,15 @@ def register(url_prefix="/auth"):
except SQLAlchemyError: except SQLAlchemyError:
current_app.logger.exception("[auth] failed to revoke grants") 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(SESSION_USER_KEY, None)
qsession.pop(ACCOUNT_SESSION_KEY, None) qsession.pop(ACCOUNT_SESSION_KEY, None)
from shared.infrastructure.urls import blog_url from shared.infrastructure.urls import blog_url