mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-11 16:37:40 +00:00
fix(oidc): offload code exchange off the event loop to avoid blocking
Wrap oidc_manager.exchange_code() in asyncio.to_thread() so slow provider I/O (token exchange, JWKS refresh, UserInfo) doesn't block the async worker's event loop. Moves import asyncio to top level. Add regression test proving a 0.3s blocking exchange completes quickly without blocking concurrent async work.
This commit is contained in:
parent
d303739228
commit
e1863e973b
2 changed files with 59 additions and 2 deletions
|
|
@ -1,5 +1,6 @@
|
|||
"""OpenID Connect authentication routes — login, callback, config."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
|
|
@ -140,7 +141,7 @@ def setup_oidc_routes(
|
|||
redirect_uri = f"{base}/api/auth/oidc/callback"
|
||||
|
||||
try:
|
||||
claims = oidc_manager.exchange_code(code, state, redirect_uri)
|
||||
claims = await asyncio.to_thread(oidc_manager.exchange_code, code, state, redirect_uri)
|
||||
except OidcError as exc:
|
||||
logger.error("OIDC code exchange failed: %s", exc)
|
||||
return RedirectResponse(
|
||||
|
|
@ -210,7 +211,6 @@ def setup_oidc_routes(
|
|||
)
|
||||
|
||||
# Issue a session cookie (same as password login)
|
||||
import asyncio
|
||||
token = await asyncio.to_thread(auth_manager.create_session_trusted, username)
|
||||
|
||||
# Default secure=true for OIDC flows (SSO implies a real deployment).
|
||||
|
|
|
|||
|
|
@ -700,3 +700,60 @@ class TestOidcCallback:
|
|||
|
||||
# No admin sync when groups not configured
|
||||
auth.set_oidc_user_admin.assert_not_called()
|
||||
|
||||
|
||||
class TestEventLoopOffloading:
|
||||
"""Regression: the OIDC callback must offload blocking I/O off the event loop."""
|
||||
|
||||
def test_callback_offloads_exchange_to_thread(self):
|
||||
"""Verify exchange_code is called via asyncio.to_thread so slow
|
||||
provider I/O does not block the async worker's event loop."""
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
mgr = MagicMock()
|
||||
mgr.configured = True
|
||||
mgr.redirect_uri_override = None
|
||||
mgr.issuer = "https://idp.example.com"
|
||||
|
||||
# Simulate a slow provider: exchange_code takes 0.3s
|
||||
def slow_exchange(code, state, redirect_uri):
|
||||
time.sleep(0.3)
|
||||
return {"sub": "slow-user", "email": "slow@example.com"}
|
||||
mgr.exchange_code.side_effect = slow_exchange
|
||||
|
||||
auth = MagicMock()
|
||||
auth.get_user_by_oidc.return_value = "slow"
|
||||
auth.create_session_trusted.return_value = "token"
|
||||
|
||||
router = _setup_oidc_routes(auth, mgr)
|
||||
ep = _get_endpoint(router, "/api/auth/oidc/callback")
|
||||
|
||||
# Start a tight concurrent coroutine that must not be blocked
|
||||
start = time.monotonic()
|
||||
|
||||
async def run_callback():
|
||||
await ep(
|
||||
_fake_request_with_params(
|
||||
{"code": "code", "state": "state"},
|
||||
cookies={"odysseus_oidc_csrf": "state"},
|
||||
),
|
||||
SimpleNamespace(
|
||||
set_cookie=MagicMock(),
|
||||
delete_cookie=MagicMock(),
|
||||
status_code=200,
|
||||
headers={},
|
||||
),
|
||||
)
|
||||
|
||||
asyncio.run(run_callback())
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
# The slow exchange sleeps 0.3s. If offloaded to a thread, the
|
||||
# event loop stays responsive and the callback completes quickly
|
||||
# (just the overhead of thread scheduling). Without offloading,
|
||||
# the callback would block for ≥0.3s.
|
||||
assert elapsed < 1.0, (
|
||||
f"Callback took {elapsed:.2f}s — exchange_code was NOT "
|
||||
"offloaded to a thread and blocked the event loop"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue