headroomlabs-ai/headroom · error · ValueError

token_url must be https (loopback http allowed for tests; se

Error message

token_url must be https (loopback http allowed for tests; set allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override)

What it means

By default the OAuth2 provider refuses non-https token URLs (only https, or loopback http for tests, pass `_https_or_local`), because posting client credentials over plaintext http leaks them. The error message names both escapes: `allow_insecure=True` on the constructor or `HEADROOM_OAUTH2_ALLOW_INSECURE=1` — intended for local dev/test only.

Source

Thrown at plugins/headroom-oauth2/src/headroom_oauth2/provider.py:64

        client_id: str,
        client_secret: str,
        scopes=None,
        audience: str | None = None,
        grant_type: str = "client_credentials",
        auth_style: str = "post",
        extra_params=None,
        skew_seconds: int = 60,
        timeout_seconds: float = 30.0,
        allow_insecure: bool = False,
    ):
        if not token_url:
            raise ValueError("token_url is required")
        if not client_id or not client_secret:
            raise ValueError("client_id and client_secret are required")
        if auth_style not in ("post", "basic"):
            raise ValueError("auth_style must be 'post' or 'basic'")
        if not allow_insecure and not _https_or_local(token_url):
            raise ValueError(
                "token_url must be https (loopback http allowed for tests; set "
                "allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override)"
            )
        self.token_url = token_url
        self.client_id = client_id
        self.client_secret = client_secret
        self.scopes = list(scopes or [])
        self.audience = audience
        self.grant_type = grant_type
        self.auth_style = auth_style
        self.extra_params = dict(extra_params or {})
        self.skew = max(0, int(skew_seconds))
        self.timeout = timeout_seconds
        self._lock = threading.Lock()
        self._token: str | None = None
        self._exp = 0.0
        self._eff_skew = self.skew

View on GitHub (pinned to 322425c43b)

Solutions

  1. Preferred: give the IdP a real https URL (proper cert or trusted internal CA) and use it as token_url
  2. Dev/test only: acknowledge the risk and set `HEADROOM_OAUTH2_ALLOW_INSECURE=1` (or `allow_insecure=True`), scoped strictly to non-production environments
  3. If a reverse proxy causes the http hop, configure the provider to target the https listener rather than the plaintext upstream

Example fix

# before
provider = OAuth2ClientCredentials(
    token_url="http://keycloak:8080/realms/main/protocol/openid-connect/token", ...)

# after (dev only)
export HEADROOM_OAUTH2_ALLOW_INSECURE=1
provider = OAuth2ClientCredentials(
    token_url="http://keycloak:8080/realms/main/protocol/openid-connect/token", ...)
# production: token_url="https://idp.example.com/realms/main/protocol/openid-connect/token"
Defensive patterns

Strategy: validation

Validate before calling

from urllib.parse import urlparse

def token_url_ok(url: str, allow_insecure: bool = False) -> bool:
    if allow_insecure:
        return True
    p = urlparse(url)
    host = p.hostname or ""
    return p.scheme == "https" or host in {"127.0.0.1", "localhost", "::1"}

if not token_url_ok(url):
    raise RuntimeError("refusing plaintext token_url outside dev; set https or ALLOW_INSECURE=1 for local only")

Type guard

def is_https_or_local(url: str) -> bool:
    p = urlparse(url)
    return p.scheme == "https" or (p.hostname or "") in {"127.0.0.1", "localhost", "::1"}

Try / catch

try:
    provider = OAuth2ClientCredentials(token_url=url, ...)
except ValueError as e:
    if "must be https" in str(e):
        if os.environ.get("ENV") == "development":
            provider = OAuth2ClientCredentials(token_url=url, allow_insecure=True, ...)
        else:
            raise
    else:
        raise

Prevention

When it happens

Trigger: Constructing the provider with `token_url="http://idp.internal:8080/token"` (plain http to a non-loopback host) while `allow_insecure` is False — typical when pointing at an internal IdP during development, or when a proxy rewrites https URLs to http upstream.

Common situations: Local docker-compose stacks where the IdP is `http://keycloak:8080/...`; corporate networks with TLS termination upstream so the client sees http; forgetting that loopback (`http://127.0.0.1`, `http://localhost`) is allowed but container hostnames are not; prod configs accidentally using an http internal DNS name.

Related errors


AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15). Data as JSON: /api/errors/562f0a14eecd9001. Report an issue: GitHub.