Fix OAuth token exchange: use internal URL, add error logging
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m50s

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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-25 01:20:41 +00:00
parent 3dde4e79ab
commit b45a2b6c10
4 changed files with 32 additions and 4 deletions

View File

@@ -64,6 +64,11 @@ class Settings:
default_factory=lambda: os.environ.get("SECRET_KEY", "change-me-in-production") 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 # GPU/Streaming settings
streaming_gpu_persist: bool = field( streaming_gpu_persist: bool = field(
default_factory=lambda: os.environ.get("STREAMING_GPU_PERSIST", "0") == "1" default_factory=lambda: os.environ.get("STREAMING_GPU_PERSIST", "0") == "1"

View File

@@ -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 GET /auth/logout — clear cookie, redirect through account SSO logout
""" """
import logging
import secrets import secrets
import time import time
@@ -18,6 +19,7 @@ from artdag_common.middleware.auth import UserContext, set_auth_cookie, clear_au
from ..config import settings from ..config import settings
logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
_signer = None _signer = None
@@ -119,24 +121,32 @@ async def callback(request: Request):
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
# Exchange code for user info via account's token endpoint # 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: async with httpx.AsyncClient(timeout=10) as client:
try: try:
resp = await client.post( resp = await client.post(
settings.oauth_token_url, token_url,
json={ json={
"code": code, "code": code,
"client_id": settings.oauth_client_id, "client_id": settings.oauth_client_id,
"redirect_uri": settings.oauth_redirect_uri, "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) return RedirectResponse(url="/", status_code=302)
if resp.status_code != 200: 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) return RedirectResponse(url="/", status_code=302)
data = resp.json() data = resp.json()
if "error" in data: if "error" in data:
logger.error("OAuth token exchange error: %s", data["error"])
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
# Map OAuth response to artdag UserContext # Map OAuth response to artdag UserContext

View File

@@ -41,6 +41,9 @@ class Settings:
oauth_logout_url: str = os.environ.get("OAUTH_LOGOUT_URL", "https://account.rose-ash.com/auth/sso-logout/") 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") 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): def __post_init__(self):
# Parse L1 servers # Parse L1 servers
l1_str = os.environ.get("L1_SERVERS", "https://celery-artdag.rose-ash.com") l1_str = os.environ.get("L1_SERVERS", "https://celery-artdag.rose-ash.com")

View File

@@ -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 GET /auth/logout — clear cookie, redirect through account SSO logout
""" """
import logging
import secrets import secrets
import time import time
@@ -18,6 +19,7 @@ from artdag_common.middleware.auth import UserContext, set_auth_cookie, clear_au
from ..config import settings from ..config import settings
logger = logging.getLogger(__name__)
router = APIRouter() router = APIRouter()
_signer = None _signer = None
@@ -119,24 +121,32 @@ async def callback(request: Request):
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
# Exchange code for user info via account's token endpoint # 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: async with httpx.AsyncClient(timeout=10) as client:
try: try:
resp = await client.post( resp = await client.post(
settings.oauth_token_url, token_url,
json={ json={
"code": code, "code": code,
"client_id": settings.oauth_client_id, "client_id": settings.oauth_client_id,
"redirect_uri": settings.oauth_redirect_uri, "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) return RedirectResponse(url="/", status_code=302)
if resp.status_code != 200: 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) return RedirectResponse(url="/", status_code=302)
data = resp.json() data = resp.json()
if "error" in data: if "error" in data:
logger.error("OAuth token exchange error: %s", data["error"])
return RedirectResponse(url="/", status_code=302) return RedirectResponse(url="/", status_code=302)
# Map OAuth response to artdag UserContext # Map OAuth response to artdag UserContext