fix: asyncio lock on callback to prevent concurrent duplicate code exchange
All checks were successful
Build and Deploy / deploy (push) Successful in 1m2s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m2s
This commit is contained in:
@@ -40,7 +40,10 @@ def _session(request: Request) -> dict:
|
|||||||
# Server-side token store — keeps JWTs OUT of the session cookie
|
# Server-side token store — keeps JWTs OUT of the session cookie
|
||||||
# (cookie limit is 4KB; two JWTs alone are ~1.3KB before base64 overhead)
|
# (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}
|
_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:
|
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}")
|
print(f"[CALLBACK] Tink returned error: {error}")
|
||||||
return RedirectResponse(f"/demo/step/3?error={error}")
|
return RedirectResponse(f"/demo/step/3?error={error}")
|
||||||
if code:
|
if code:
|
||||||
# Guard: if we already have a user_token for this session, the code was
|
sid = sess.get("sid", "unknown")
|
||||||
# already exchanged (duplicate callback from Traefik during rolling deploy).
|
if sid not in _callback_locks:
|
||||||
if _load_token(sess, "user_token"):
|
_callback_locks[sid] = asyncio.Lock()
|
||||||
print(f"[CALLBACK] Already have user_token — skipping duplicate exchange")
|
async with _callback_locks[sid]:
|
||||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
if _load_token(sess, "user_token"):
|
||||||
try:
|
print(f"[CALLBACK] Already have user_token — skipping duplicate exchange")
|
||||||
s = get_settings()
|
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
||||||
print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
|
try:
|
||||||
client = _client(log_cb=_logger(sess))
|
s = get_settings()
|
||||||
tokens = await client.exchange_code_for_token(
|
print(f"[CALLBACK] Exchanging code, redirect_uri={s.tink_redirect_uri!r}")
|
||||||
code, redirect_uri=s.tink_redirect_uri
|
client = _client(log_cb=_logger(sess))
|
||||||
)
|
tokens = await client.exchange_code_for_token(
|
||||||
print(f"[CALLBACK] Token response keys: {list(tokens.keys())}")
|
code, redirect_uri=s.tink_redirect_uri
|
||||||
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] Token response keys: {list(tokens.keys())}")
|
||||||
print(f"[CALLBACK] SUCCESS — user_token saved ({len(user_token)} chars)")
|
user_token = tokens.get("access_token", "")
|
||||||
return RedirectResponse("/demo/step/3?cb_success=1", status_code=303)
|
if not user_token:
|
||||||
except Exception as e:
|
print(f"[CALLBACK] ERROR: access_token missing, got: {tokens}")
|
||||||
import traceback
|
return RedirectResponse(
|
||||||
print(f"[CALLBACK] EXCEPTION: {e}\n{traceback.format_exc()}")
|
f"/demo/step/3?cb_error=Token+exchange+ok+but+access_token+missing+in+response",
|
||||||
return RedirectResponse(f"/demo/step/3?cb_error={str(e)}", status_code=303)
|
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")
|
print(f"[CALLBACK] No code — bare redirect to step 3")
|
||||||
return RedirectResponse("/demo/step/3", status_code=303)
|
return RedirectResponse("/demo/step/3", status_code=303)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user