diff --git a/src/routes/demo.py b/src/routes/demo.py index 786bc4e..d60a28c 100644 --- a/src/routes/demo.py +++ b/src/routes/demo.py @@ -40,7 +40,10 @@ def _session(request: Request) -> dict: # Server-side token store — keeps JWTs OUT of the session cookie # (cookie limit is 4KB; two JWTs alone are ~1.3KB before base64 overhead) # --------------------------------------------------------------------------- +import asyncio + _token_store: dict[str, dict] = {} # sid → {"app_token": str, "user_token": str} +_callback_locks: dict[str, asyncio.Lock] = {} # sid → Lock (prevents concurrent code exchange) def _get_sid(sess: dict) -> str: @@ -402,33 +405,35 @@ async def tink_callback(request: Request, code: Optional[str] = None, print(f"[CALLBACK] Tink returned error: {error}") return RedirectResponse(f"/demo/step/3?error={error}") if code: - # Guard: if we already have a user_token for this session, the code was - # already exchanged (duplicate callback from Traefik during rolling deploy). - if _load_token(sess, "user_token"): - print(f"[CALLBACK] Already have user_token — skipping duplicate exchange") - return RedirectResponse("/demo/step/3?cb_success=1", status_code=303) - try: - s = get_settings() - print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}") - client = _client(log_cb=_logger(sess)) - tokens = await client.exchange_code_for_token( - code, redirect_uri=s.tink_redirect_uri - ) - print(f"[CALLBACK] Token response keys: {list(tokens.keys())}") - user_token = tokens.get("access_token", "") - if not user_token: - print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}") - return RedirectResponse( - f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response", - status_code=303, + sid = sess.get("sid", "unknown") + if sid not in _callback_locks: + _callback_locks[sid] = asyncio.Lock() + async with _callback_locks[sid]: + if _load_token(sess, "user_token"): + print(f"[CALLBACK] Already have user_token — skipping duplicate exchange") + return RedirectResponse("/demo/step/3?cb_success=1", status_code=303) + try: + s = get_settings() + print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}") + client = _client(log_cb=_logger(sess)) + tokens = await client.exchange_code_for_token( + code, redirect_uri=s.tink_redirect_uri ) - _store_token(sess, "user_token", user_token) - print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)") - return RedirectResponse("/demo/step/3?cb_success=1", status_code=303) - except Exception as e: - import traceback - print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}") - return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303) + print(f"[CALLBACK] Token response keys: {list(tokens.keys())}") + user_token = tokens.get("access_token", "") + if not user_token: + print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}") + return RedirectResponse( + f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response", + status_code=303, + ) + _store_token(sess, "user_token", user_token) + print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)") + return RedirectResponse("/demo/step/3?cb_success=1", status_code=303) + except Exception as e: + import traceback + print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}") + return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303) print(f"[CALLBACK] No code — bare redirect to step 3") return RedirectResponse("/demo/step/3", status_code=303)