From b45a2b6c10d98f2f5a9a7511519b10c5d94975cf Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 25 Feb 2026 01:20:41 +0000 Subject: [PATCH] Fix OAuth token exchange: use internal URL, add error logging The server-to-server token exchange was hitting the external URL (https://account.rose-ash.com/...) which can fail from inside Docker due to DNS/hairpin NAT. Now uses INTERNAL_URL_ACCOUNT (already set in both docker-compose files) for the POST. Adds logging at all three failure points so silent redirects are diagnosable. Co-Authored-By: Claude Opus 4.6 --- l1/app/config.py | 5 +++++ l1/app/routers/auth.py | 14 ++++++++++++-- l2/app/config.py | 3 +++ l2/app/routers/auth.py | 14 ++++++++++++-- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/l1/app/config.py b/l1/app/config.py index 8aa94d7..6e4c005 100644 --- a/l1/app/config.py +++ b/l1/app/config.py @@ -64,6 +64,11 @@ class Settings: default_factory=lambda: os.environ.get("SECRET_KEY", "change-me-in-production") ) + # Internal account URL for server-to-server token exchange (avoids external DNS/TLS) + internal_account_url: str = field( + default_factory=lambda: os.environ.get("INTERNAL_URL_ACCOUNT", "") + ) + # GPU/Streaming settings streaming_gpu_persist: bool = field( default_factory=lambda: os.environ.get("STREAMING_GPU_PERSIST", "0") == "1" diff --git a/l1/app/routers/auth.py b/l1/app/routers/auth.py index c447f3d..3f85658 100644 --- a/l1/app/routers/auth.py +++ b/l1/app/routers/auth.py @@ -6,6 +6,7 @@ GET /auth/callback — exchange code for user info, set session cookie GET /auth/logout — clear cookie, redirect through account SSO logout """ +import logging import secrets import time @@ -18,6 +19,7 @@ from artdag_common.middleware.auth import UserContext, set_auth_cookie, clear_au from ..config import settings +logger = logging.getLogger(__name__) router = APIRouter() _signer = None @@ -119,24 +121,32 @@ async def callback(request: Request): return RedirectResponse(url="/", status_code=302) # Exchange code for user info via account's token endpoint + # Prefer internal URL (Docker overlay) to avoid external DNS/TLS issues + token_url = settings.oauth_token_url + if settings.internal_account_url: + token_url = f"{settings.internal_account_url.rstrip('/')}/auth/oauth/token" + async with httpx.AsyncClient(timeout=10) as client: try: resp = await client.post( - settings.oauth_token_url, + token_url, json={ "code": code, "client_id": settings.oauth_client_id, "redirect_uri": settings.oauth_redirect_uri, }, ) - except httpx.HTTPError: + except httpx.HTTPError as exc: + logger.error("OAuth token exchange failed: %s %s", type(exc).__name__, exc) return RedirectResponse(url="/", status_code=302) if resp.status_code != 200: + logger.error("OAuth token exchange returned %s: %s", resp.status_code, resp.text[:200]) return RedirectResponse(url="/", status_code=302) data = resp.json() if "error" in data: + logger.error("OAuth token exchange error: %s", data["error"]) return RedirectResponse(url="/", status_code=302) # Map OAuth response to artdag UserContext diff --git a/l2/app/config.py b/l2/app/config.py index d08b3ed..d2c1437 100644 --- a/l2/app/config.py +++ b/l2/app/config.py @@ -41,6 +41,9 @@ class Settings: oauth_logout_url: str = os.environ.get("OAUTH_LOGOUT_URL", "https://account.rose-ash.com/auth/sso-logout/") secret_key: str = os.environ.get("SECRET_KEY", "change-me-in-production") + # Internal account URL for server-to-server token exchange (avoids external DNS/TLS) + internal_account_url: str = os.environ.get("INTERNAL_URL_ACCOUNT", "") + def __post_init__(self): # Parse L1 servers l1_str = os.environ.get("L1_SERVERS", "https://celery-artdag.rose-ash.com") diff --git a/l2/app/routers/auth.py b/l2/app/routers/auth.py index 98fea5a..be7715c 100644 --- a/l2/app/routers/auth.py +++ b/l2/app/routers/auth.py @@ -6,6 +6,7 @@ GET /auth/callback — exchange code for user info, set session cookie GET /auth/logout — clear cookie, redirect through account SSO logout """ +import logging import secrets import time @@ -18,6 +19,7 @@ from artdag_common.middleware.auth import UserContext, set_auth_cookie, clear_au from ..config import settings +logger = logging.getLogger(__name__) router = APIRouter() _signer = None @@ -119,24 +121,32 @@ async def callback(request: Request): return RedirectResponse(url="/", status_code=302) # Exchange code for user info via account's token endpoint + # Prefer internal URL (Docker overlay) to avoid external DNS/TLS issues + token_url = settings.oauth_token_url + if settings.internal_account_url: + token_url = f"{settings.internal_account_url.rstrip('/')}/auth/oauth/token" + async with httpx.AsyncClient(timeout=10) as client: try: resp = await client.post( - settings.oauth_token_url, + token_url, json={ "code": code, "client_id": settings.oauth_client_id, "redirect_uri": settings.oauth_redirect_uri, }, ) - except httpx.HTTPError: + except httpx.HTTPError as exc: + logger.error("OAuth token exchange failed: %s %s", type(exc).__name__, exc) return RedirectResponse(url="/", status_code=302) if resp.status_code != 200: + logger.error("OAuth token exchange returned %s: %s", resp.status_code, resp.text[:200]) return RedirectResponse(url="/", status_code=302) data = resp.json() if "error" in data: + logger.error("OAuth token exchange error: %s", data["error"]) return RedirectResponse(url="/", status_code=302) # Map OAuth response to artdag UserContext