From 0e021814778c8d7f0984500959302f3e5bd8ccc2 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Mon, 30 Mar 2026 21:14:10 -0700 Subject: [PATCH] feat: add CORS middleware for cross-origin federation --- muxplex/main.py | 9 +++++++ muxplex/tests/test_api.py | 54 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/muxplex/main.py b/muxplex/main.py index 0f9c0df..a3cbedc 100644 --- a/muxplex/main.py +++ b/muxplex/main.py @@ -28,6 +28,7 @@ from fastapi.responses import HTMLResponse from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, field_validator from starlette.responses import RedirectResponse +from starlette.middleware.cors import CORSMiddleware from muxplex.auth import ( AuthMiddleware, @@ -251,6 +252,14 @@ app.add_middleware( password=_auth_password, ) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + # --------------------------------------------------------------------------- # Request / response models diff --git a/muxplex/tests/test_api.py b/muxplex/tests/test_api.py index e57aa3e..169f76b 100644 --- a/muxplex/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -1029,6 +1029,60 @@ def test_instance_info_no_auth_required(tmp_path, monkeypatch): assert "version" in data +# --------------------------------------------------------------------------- +# CORS middleware +# --------------------------------------------------------------------------- + + +def test_cors_preflight_returns_200(tmp_path, monkeypatch): + """OPTIONS /api/sessions with CORS preflight headers returns 200 with access-control-allow-origin header. + + NOTE — Spec discrepancy: the acceptance criteria states `access-control-allow-origin: *`, + but the middleware is configured with `allow_credentials=True`. The CORS specification + (RFC 6454 / Fetch standard) forbids the wildcard value when credentials are included; + Starlette therefore reflects the request Origin instead of emitting "*". Keeping + `allow_credentials=True` is intentional for cross-origin federation with session cookies, + so the assertion uses the reflected origin rather than a literal "*". + """ + monkeypatch.setenv("MUXPLEX_PASSWORD", "test-password") + origin = "http://other-muxplex.local:8088" + with TestClient(app) as c: + response = c.options( + "/api/sessions", + headers={ + "Origin": origin, + "Access-Control-Request-Method": "GET", + }, + ) + assert response.status_code == 200 + assert response.headers.get("access-control-allow-origin") == origin + + +def test_cors_allows_any_origin(client): + """GET /api/sessions with Origin header gets access-control-allow-origin header in response. + + NOTE — Spec discrepancy: the acceptance criteria states `access-control-allow-origin: *`, + but this is incompatible with `allow_credentials=True` (CORS spec forbids the two together). + Starlette reflects the request Origin instead. This provides equivalent permissiveness + (any origin is allowed) while remaining spec-compliant. + """ + origin = "http://other-muxplex.local:8088" + response = client.get( + "/api/sessions", + headers={"Origin": origin}, + ) + assert response.headers.get("access-control-allow-origin") == origin + + +def test_cors_allows_credentials(client): + """GET /api/sessions with Origin header includes access-control-allow-credentials: true.""" + response = client.get( + "/api/sessions", + headers={"Origin": "http://other-muxplex.local:8088"}, + ) + assert response.headers.get("access-control-allow-credentials") == "true" + + # --------------------------------------------------------------------------- # POST /api/sessions (create new session) # ---------------------------------------------------------------------------