Device cookie + internal endpoint for auth state detection
Each client app sets a persistent first-party device cookie ({app}_did).
On each request:
- Logged in: verify grant via account internal endpoint (cached 60s)
- Not logged in + device cookie: check-device endpoint detects if user
logged in since last grant revocation → triggers OAuth automatically
No cross-domain cookies. No propagation chain. Each app checks independently.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,54 +123,84 @@ def create_base_app(
|
||||
for fn in before_request_fns:
|
||||
app.before_request(fn)
|
||||
|
||||
# Grant revocation check: client apps verify their grant is still valid
|
||||
# Auth state check via device cookie + account internal endpoint
|
||||
if name != "account":
|
||||
@app.before_request
|
||||
async def _check_grant():
|
||||
async def _check_auth_state():
|
||||
from quart import session as qs
|
||||
grant_token = qs.get("grant_token")
|
||||
if not grant_token or not qs.get("uid"):
|
||||
return
|
||||
if request.path.startswith("/auth/"):
|
||||
from urllib.parse import quote as _quote
|
||||
if request.path.startswith("/auth/") or request.path.startswith("/static/"):
|
||||
return
|
||||
|
||||
# Check Redis cache first (avoid hammering account on every request)
|
||||
cache_key = f"grant:{grant_token}"
|
||||
uid = qs.get("uid")
|
||||
grant_token = qs.get("grant_token")
|
||||
device_id = request.cookies.get(f"{name}_did")
|
||||
|
||||
import os, aiohttp
|
||||
from shared.browser.app.redis_cacher import get_redis
|
||||
redis = get_redis()
|
||||
if redis:
|
||||
cached = await redis.get(cache_key)
|
||||
if cached == b"ok":
|
||||
return
|
||||
if cached == b"revoked":
|
||||
account_internal = (os.getenv("INTERNAL_URL_ACCOUNT") or "http://account:8000").rstrip("/")
|
||||
|
||||
# Case 1: logged in locally — verify grant still valid
|
||||
if uid and grant_token:
|
||||
cache_key = f"grant:{grant_token}"
|
||||
if redis:
|
||||
cached = await redis.get(cache_key)
|
||||
if cached == b"ok":
|
||||
return
|
||||
if cached == b"revoked":
|
||||
qs.pop("uid", None)
|
||||
qs.pop("grant_token", None)
|
||||
qs.pop("cart_sid", None)
|
||||
return
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http:
|
||||
async with http.get(
|
||||
f"{account_internal}/auth/internal/verify-grant",
|
||||
params={"token": grant_token},
|
||||
timeout=aiohttp.ClientTimeout(total=3),
|
||||
) as resp:
|
||||
data = await resp.json()
|
||||
valid = data.get("valid", False)
|
||||
except Exception:
|
||||
return # account unreachable — don't log user out
|
||||
|
||||
if redis:
|
||||
await redis.set(cache_key, b"ok" if valid else b"revoked", ex=60)
|
||||
if not valid:
|
||||
qs.pop("uid", None)
|
||||
qs.pop("grant_token", None)
|
||||
qs.pop("cart_sid", None)
|
||||
return
|
||||
|
||||
# Call account's internal endpoint
|
||||
import os, aiohttp
|
||||
account_internal = (os.getenv("INTERNAL_URL_ACCOUNT") or "http://account:8000").rstrip("/")
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http:
|
||||
async with http.get(
|
||||
f"{account_internal}/auth/internal/verify-grant",
|
||||
params={"token": grant_token},
|
||||
timeout=aiohttp.ClientTimeout(total=3),
|
||||
) as resp:
|
||||
data = await resp.json()
|
||||
valid = data.get("valid", False)
|
||||
except Exception:
|
||||
# If account is unreachable, don't log user out
|
||||
return
|
||||
|
||||
if redis:
|
||||
await redis.set(cache_key, b"ok" if valid else b"revoked", ex=60)
|
||||
# Case 2: not logged in but device cookie exists — check for auth change
|
||||
if not uid and device_id:
|
||||
cache_key = f"device:{name}:{device_id}"
|
||||
if redis:
|
||||
cached = await redis.get(cache_key)
|
||||
if cached == b"none":
|
||||
return # recently checked, no auth
|
||||
if cached == b"active":
|
||||
# Auth available — trigger OAuth
|
||||
return redirect(f"/auth/login/?next={_quote(request.url, safe='')}")
|
||||
|
||||
if not valid:
|
||||
qs.pop("uid", None)
|
||||
qs.pop("grant_token", None)
|
||||
qs.pop("cart_sid", None)
|
||||
try:
|
||||
async with aiohttp.ClientSession() as http:
|
||||
async with http.get(
|
||||
f"{account_internal}/auth/internal/check-device",
|
||||
params={"device_id": device_id, "app": name},
|
||||
timeout=aiohttp.ClientTimeout(total=3),
|
||||
) as resp:
|
||||
data = await resp.json()
|
||||
active = data.get("active", False)
|
||||
except Exception:
|
||||
return # account unreachable — stay anonymous
|
||||
|
||||
if redis:
|
||||
await redis.set(cache_key, b"active" if active else b"none", ex=60)
|
||||
if active:
|
||||
return redirect(f"/auth/login/?next={_quote(request.url, safe='')}")
|
||||
|
||||
@app.before_request
|
||||
async def _csrf_protect():
|
||||
|
||||
Reference in New Issue
Block a user