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
- Preferred: give the IdP a real https URL (proper cert or trusted internal CA) and use it as token_url
- Dev/test only: acknowledge the risk and set `HEADROOM_OAUTH2_ALLOW_INSECURE=1` (or `allow_insecure=True`), scoped strictly to non-production environments
- 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
- Use https token URLs everywhere; treat ALLOW_INSECURE as a dev-only flag blocked in prod config review
- Give local IdPs a trusted cert (mkcert) instead of downgrading security
- In containers, remember loopback exception covers 127.0.0.1 only — service names like http://keycloak:8080 still fail
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
- Invalid {TRUSTED_DASHBOARD_CLIENT_CIDRS_ENV} entry: {exc}
- {key}={raw!r} is not an integer
- headroom-oauth2 misconfigured: {e}
- token_url is required
- client_id and client_secret are required
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/562f0a14eecd9001.
Report an issue: GitHub.