diff --git a/api/auth.py b/api/auth.py index 39922f1b..6dbb79a3 100644 --- a/api/auth.py +++ b/api/auth.py @@ -16,7 +16,7 @@ import threading import time from pathlib import Path -from api.config import STATE_DIR, load_settings +from api.config import STATE_DIR, get_config, load_settings logger = logging.getLogger(__name__) @@ -455,6 +455,49 @@ def is_oidc_auth_enabled() -> bool: return False +def get_oidc_startup_warning() -> str | None: + """Return a startup warning when OIDC auth is only partially configured.""" + try: + cfg = get_config() + raw = cfg.get("webui_oidc") if isinstance(cfg, dict) else {} + if not isinstance(raw, dict): + raw = {} + except Exception: + logger.debug("Failed to read webui_oidc config", exc_info=True) + raw = {} + + def pick(name: str, env_name: str) -> str: + env_value = os.getenv(env_name) + value = env_value if env_value is not None else raw.get(name) + return str(value or "").strip() + + issuer = bool(pick("issuer", "HERMES_WEBUI_OIDC_ISSUER")) + client_id = bool(pick("client_id", "HERMES_WEBUI_OIDC_CLIENT_ID")) + allow_claim = bool(pick("allow_claim", "HERMES_WEBUI_OIDC_ALLOW_CLAIM")) + allow_values = bool(pick("allow_values", "HERMES_WEBUI_OIDC_ALLOW_VALUES")) + + if not any((issuer, client_id, allow_claim, allow_values)): + return None + if issuer and client_id and allow_claim and allow_values: + return None + + missing = [] + if not issuer: + missing.append("issuer") + if not client_id: + missing.append("client_id") + if not allow_claim: + missing.append("allow_claim") + if not allow_values: + missing.append("allow_values") + + joined = ", ".join(missing) + return ( + "Native OIDC login is only partially configured; missing " + f"{joined}. The WebUI will not enable OIDC auth until all four fields are set." + ) + + def is_auth_enabled() -> bool: """True if password auth, passkeys, or OIDC login is configured.""" return ( diff --git a/api/auth_oidc.py b/api/auth_oidc.py index 58cb8fe5..7eee5748 100644 --- a/api/auth_oidc.py +++ b/api/auth_oidc.py @@ -54,34 +54,6 @@ def is_oidc_enabled() -> bool: and cfg.get("allow_values") ) -def get_oidc_startup_warning() -> str | None: - cfg = _resolve_oidc_config() - issuer = bool(cfg.get("issuer")) - client_id = bool(cfg.get("client_id")) - allow_claim = bool(cfg.get("allow_claim")) - allow_values = bool(cfg.get("allow_values")) - - if not any((issuer, client_id, allow_claim, allow_values)): - return None - if issuer and client_id and allow_claim and allow_values: - return None - - missing = [] - if not issuer: - missing.append("issuer") - if not client_id: - missing.append("client_id") - if not allow_claim: - missing.append("allow_claim") - if not allow_values: - missing.append("allow_values") - - joined = ", ".join(missing) - return ( - "Native OIDC login is only partially configured; missing " - f"{joined}. The WebUI will not enable OIDC auth until all four fields are set." - ) - def build_authorization_redirect( request_base_url: str, diff --git a/server.py b/server.py index ba505b2d..05860d6d 100644 --- a/server.py +++ b/server.py @@ -584,8 +584,8 @@ def main() -> None: if within_container: print('[ok] Running within container.', flush=True) - from api.auth import is_auth_enabled - from api.auth_oidc import get_oidc_startup_warning + # Security: warn if binding non-loopback without authentication + from api.auth import get_oidc_startup_warning, is_auth_enabled if HOST not in ('127.0.0.1', '::1', 'localhost') and not is_auth_enabled(): print(f'[!!] WARNING: Binding to {HOST} with NO PASSWORD SET.', flush=True) print(f' Anyone on the network can access your filesystem and agent.', flush=True) diff --git a/tests/test_issue3825_oidc_auth.py b/tests/test_issue3825_oidc_auth.py index 022cb029..d6545dbf 100644 --- a/tests/test_issue3825_oidc_auth.py +++ b/tests/test_issue3825_oidc_auth.py @@ -246,10 +246,10 @@ def test_oidc_enablement_requires_explicit_allowlist(monkeypatch): assert auth_oidc.is_oidc_enabled() is False def test_oidc_startup_warning_flags_partial_config(monkeypatch): - import api.auth_oidc as auth_oidc + import api.auth as auth monkeypatch.setattr( - auth_oidc, + auth, "get_config", lambda: { "webui_oidc": { @@ -259,16 +259,16 @@ def test_oidc_startup_warning_flags_partial_config(monkeypatch): }, ) - warning = auth_oidc.get_oidc_startup_warning() + warning = auth.get_oidc_startup_warning() assert warning is not None assert "allow_claim" in warning assert "allow_values" in warning def test_oidc_startup_warning_ignores_complete_config(monkeypatch): - import api.auth_oidc as auth_oidc + import api.auth as auth monkeypatch.setattr( - auth_oidc, + auth, "get_config", lambda: { "webui_oidc": { @@ -280,7 +280,7 @@ def test_oidc_startup_warning_ignores_complete_config(monkeypatch): }, ) - assert auth_oidc.get_oidc_startup_warning() is None + assert auth.get_oidc_startup_warning() is None def test_validate_id_token_rejects_mismatched_jwk_key_family(monkeypatch): import api.auth_oidc as auth_oidc