From 1f578a4827e5cc48cc8efaf609a436c8fbe92295 Mon Sep 17 00:00:00 2001 From: MervinPraison <454862+MervinPraison@users.noreply.github.com> Date: Tue, 28 Jul 2026 10:16:03 +0000 Subject: [PATCH] fix: make URL-token auth opt-in so env token no longer forces 401 (fixes #251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TokenQueryMiddleware previously activated whenever GATEWAY_AUTH_TOKEN or AIUI_URL_TOKEN was present in the process environment, forcing 401 on all standalone /api/* routes. A token leaking into a developer shell after a gateway run broke ad-hoc TestClient scripts (#238, #251). Activation now requires explicit opt-in: AIUI_REQUIRE_TOKEN, YAML auth.requireToken, or require_auth=True. Secured deployments keep full Bearer / query-param / cookie token auth. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/praisonaiui/server.py | 28 ++++++++++-- tests/unit/test_token_auth_optin.py | 67 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 tests/unit/test_token_auth_optin.py diff --git a/src/praisonaiui/server.py b/src/praisonaiui/server.py index f955476..3dd4bf2 100644 --- a/src/praisonaiui/server.py +++ b/src/praisonaiui/server.py @@ -2860,7 +2860,14 @@ def create_app( require_auth: bool = False, config_path: Optional[Path] = None, ) -> Starlette: - """Create the Starlette application.""" + """Create the Starlette application. + + URL-token auth (``TokenQueryMiddleware``) is opt-in: it activates only + when ``AIUI_REQUIRE_TOKEN`` is truthy, YAML ``auth.requireToken`` is set, + or ``require_auth=True``. A bare ``GATEWAY_AUTH_TOKEN`` / + ``AIUI_URL_TOKEN`` env var (e.g. leaked into a developer shell after a + gateway run) no longer forces 401 on standalone ``/api/*`` (#238, #251). + """ from praisonaiui.auth import ( AuthMiddleware, login_handler, @@ -2874,10 +2881,13 @@ def create_app( config = load_config_from_yaml(config_path) # Extract auth settings from config + _config_requires_token = False if config: auth_config = config.get("auth", {}) if auth_config.get("requireAuth") or auth_config.get("require_auth"): require_auth = True + if auth_config.get("requireToken") or auth_config.get("require_token"): + _config_requires_token = True middleware = [ Middleware( @@ -2891,10 +2901,20 @@ def create_app( # Add optional AUTH_ENFORCE middleware (env var driven) middleware.append(Middleware(AuthEnforcementMiddleware)) - # URL-token auth — opt-in via env. Enables query-param / cookie auth so - # dashboards can be shared by URL (Jupyter model). + # URL-token auth — opt-in. Enables query-param / cookie auth so dashboards + # can be shared by URL (Jupyter model). The mere presence of a token env + # var (e.g. GATEWAY_AUTH_TOKEN leaking into a developer shell after a + # gateway run) must NOT force auth on standalone create_app() — that broke + # ad-hoc TestClient scripts (#238, #251). Activation now requires an + # explicit opt-in: AIUI_REQUIRE_TOKEN=1, YAML auth.requireToken, or the + # require_auth flag. _tok = os.environ.get("AIUI_URL_TOKEN") or os.environ.get("GATEWAY_AUTH_TOKEN") - if _tok: + _token_auth_opt_in = ( + os.environ.get("AIUI_REQUIRE_TOKEN", "").lower() in ("true", "1", "yes") + or _config_requires_token + or require_auth + ) + if _tok and _token_auth_opt_in: from praisonaiui.auth import TokenQueryMiddleware middleware.append( Middleware(TokenQueryMiddleware, expected_token=_tok) diff --git a/tests/unit/test_token_auth_optin.py b/tests/unit/test_token_auth_optin.py new file mode 100644 index 0000000..dec2130 --- /dev/null +++ b/tests/unit/test_token_auth_optin.py @@ -0,0 +1,67 @@ +"""Regression tests for opt-in token auth activation (#238, #251). + +The mere presence of GATEWAY_AUTH_TOKEN / AIUI_URL_TOKEN in the environment +(e.g. leaking into a developer shell after a gateway run) must not force 401 +on standalone create_app() + TestClient. Token auth activates only when +explicitly opted in via AIUI_REQUIRE_TOKEN, YAML auth.requireToken, or +require_auth=True. +""" + +import pytest +from starlette.testclient import TestClient + +from praisonaiui import server +from praisonaiui.server import create_app + + +@pytest.fixture(autouse=True) +def _reset_state(): + server.reset_state() + yield + server.reset_state() + + +def test_env_token_alone_does_not_force_401(monkeypatch): + monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token") + monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False) + client = TestClient(create_app()) + assert client.get("/api/agents").status_code == 200 + + +def test_aiui_url_token_alone_does_not_force_401(monkeypatch): + monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token") + monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False) + client = TestClient(create_app()) + assert client.get("/api/agents").status_code == 200 + + +def test_require_token_env_opt_in_enforces_401(monkeypatch): + monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token") + monkeypatch.setenv("AIUI_REQUIRE_TOKEN", "1") + client = TestClient(create_app()) + assert client.get("/api/agents").status_code == 401 + + +def test_require_token_env_opt_in_allows_bearer(monkeypatch): + monkeypatch.setenv("GATEWAY_AUTH_TOKEN", "test-secret-token") + monkeypatch.setenv("AIUI_REQUIRE_TOKEN", "1") + client = TestClient(create_app()) + resp = client.get( + "/api/agents", + headers={"Authorization": "Bearer test-secret-token"}, + ) + assert resp.status_code == 200 + + +def test_config_require_token_opt_in_enforces_401(monkeypatch): + monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token") + monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False) + client = TestClient(create_app(config={"auth": {"requireToken": True}})) + assert client.get("/api/agents").status_code == 401 + + +def test_require_auth_opt_in_enforces_401(monkeypatch): + monkeypatch.setenv("AIUI_URL_TOKEN", "test-secret-token") + monkeypatch.delenv("AIUI_REQUIRE_TOKEN", raising=False) + client = TestClient(create_app(require_auth=True)) + assert client.get("/api/agents").status_code == 401