Device-id SSO: account sets did, signals login via Redis

- Factory: set {name}_did cookie for all apps (including account)
  via before_request/after_request hooks (g.device_id always available)
- Factory: _check_auth_state checks did_auth:{account_did} in Redis
  to override stale "not logged in" cache when account login detected
- OAuth: removed _ensure_device_cookie (moved to factory), callback
  stores account_did from authorize redirect in session
- OAuth: login uses g.device_id, logout clears _account_did

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-23 14:57:40 +00:00
parent c4590d1442
commit cad528d732
2 changed files with 50 additions and 28 deletions

View File

@@ -8,7 +8,6 @@ client apps can detect login/logout without cross-domain cookies.
"""
from __future__ import annotations
import os
import secrets
from datetime import datetime, timezone
@@ -24,7 +23,6 @@ from quart import (
from sqlalchemy import select
from shared.db.session import get_session
from shared.models import User
from shared.models.oauth_code import OAuthCode
from shared.infrastructure.urls import account_url, app_url
from shared.infrastructure.cart_identity import current_cart_identity
@@ -34,30 +32,9 @@ SESSION_USER_KEY = "uid"
GRANT_TOKEN_KEY = "grant_token"
def _device_cookie_name(app_name: str) -> str:
return f"{app_name}_did"
def _internal_account_url() -> str:
return (os.getenv("INTERNAL_URL_ACCOUNT") or "http://account:8000").rstrip("/")
def create_oauth_blueprint(app_name: str) -> Blueprint:
"""Return an OAuth client blueprint for *app_name*."""
bp = Blueprint("oauth_auth", __name__, url_prefix="/auth")
cookie_name = _device_cookie_name(app_name)
# Ensure device cookie exists on every response
@bp.after_app_request
async def _ensure_device_cookie(response):
if not request.cookies.get(cookie_name):
did = secrets.token_urlsafe(32)
response.set_cookie(
cookie_name, did,
max_age=30 * 24 * 3600,
secure=True, samesite="Lax", httponly=True,
)
return response
@bp.get("/login")
@bp.get("/login/")
@@ -68,7 +45,7 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
qsession["oauth_state"] = state
qsession["oauth_next"] = next_url
device_id = request.cookies.get(cookie_name, "")
device_id = g.device_id
redirect_uri = app_url(app_name, "/auth/callback")
params = (
f"?client_id={app_name}"
@@ -84,6 +61,11 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
@bp.get("/callback")
@bp.get("/callback/")
async def callback():
# Always store account_did when account passes it back
account_did = request.args.get("account_did", "")
if account_did:
qsession["_account_did"] = account_did
# Handle prompt=none error (user not logged in on account)
error = request.args.get("error")
if error == "login_required":
@@ -91,7 +73,7 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
qsession.pop("oauth_state", None)
import time as _time
qsession["_pnone_at"] = _time.time()
device_id = request.cookies.get(cookie_name, "")
device_id = g.device_id
if device_id:
from shared.browser.app.redis_cacher import get_redis
_redis = get_redis()
@@ -181,7 +163,7 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
qsession.clear()
resp = await make_response(redirect("/"))
resp.delete_cookie("blog_session", domain=".rose-ash.com", path="/")
resp.delete_cookie(cookie_name, path="/")
resp.delete_cookie(f"{app_name}_did", path="/")
return resp
@bp.post("/logout")
@@ -191,6 +173,7 @@ def create_oauth_blueprint(app_name: str) -> Blueprint:
qsession.pop(GRANT_TOKEN_KEY, None)
qsession.pop("cart_sid", None)
qsession.pop("_pnone_at", None)
qsession.pop("_account_did", None)
# Redirect through account to revoke grants + clear account session
return redirect(account_url("/auth/sso-logout/"))