{"record":{"id":"562f0a14eecd9001","repo":"headroomlabs-ai/headroom","slug":"token-url-must-be-https-loopback-http-allowed-for","errorCode":null,"errorMessage":"token_url must be https (loopback http allowed for tests; set allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override)","messagePattern":"token_url must be https \\(loopback http allowed for tests; set allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override\\)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"plugins/headroom-oauth2/src/headroom_oauth2/provider.py","lineNumber":64,"sourceCode":"        client_id: str,\n        client_secret: str,\n        scopes=None,\n        audience: str | None = None,\n        grant_type: str = \"client_credentials\",\n        auth_style: str = \"post\",\n        extra_params=None,\n        skew_seconds: int = 60,\n        timeout_seconds: float = 30.0,\n        allow_insecure: bool = False,\n    ):\n        if not token_url:\n            raise ValueError(\"token_url is required\")\n        if not client_id or not client_secret:\n            raise ValueError(\"client_id and client_secret are required\")\n        if auth_style not in (\"post\", \"basic\"):\n            raise ValueError(\"auth_style must be 'post' or 'basic'\")\n        if not allow_insecure and not _https_or_local(token_url):\n            raise ValueError(\n                \"token_url must be https (loopback http allowed for tests; set \"\n                \"allow_insecure=True / HEADROOM_OAUTH2_ALLOW_INSECURE=1 to override)\"\n            )\n        self.token_url = token_url\n        self.client_id = client_id\n        self.client_secret = client_secret\n        self.scopes = list(scopes or [])\n        self.audience = audience\n        self.grant_type = grant_type\n        self.auth_style = auth_style\n        self.extra_params = dict(extra_params or {})\n        self.skew = max(0, int(skew_seconds))\n        self.timeout = timeout_seconds\n        self._lock = threading.Lock()\n        self._token: str | None = None\n        self._exp = 0.0\n        self._eff_skew = self.skew\n","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/plugins/headroom-oauth2/src/headroom_oauth2/provider.py#L46-L82","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# before\nprovider = OAuth2ClientCredentials(\n    token_url=\"http://keycloak:8080/realms/main/protocol/openid-connect/token\", ...)\n\n# after (dev only)\nexport HEADROOM_OAUTH2_ALLOW_INSECURE=1\nprovider = OAuth2ClientCredentials(\n    token_url=\"http://keycloak:8080/realms/main/protocol/openid-connect/token\", ...)\n# production: token_url=\"https://idp.example.com/realms/main/protocol/openid-connect/token\"","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\ndef token_url_ok(url: str, allow_insecure: bool = False) -> bool:\n    if allow_insecure:\n        return True\n    p = urlparse(url)\n    host = p.hostname or \"\"\n    return p.scheme == \"https\" or host in {\"127.0.0.1\", \"localhost\", \"::1\"}\n\nif not token_url_ok(url):\n    raise RuntimeError(\"refusing plaintext token_url outside dev; set https or ALLOW_INSECURE=1 for local only\")","typeGuard":"def is_https_or_local(url: str) -> bool:\n    p = urlparse(url)\n    return p.scheme == \"https\" or (p.hostname or \"\") in {\"127.0.0.1\", \"localhost\", \"::1\"}","tryCatchPattern":"try:\n    provider = OAuth2ClientCredentials(token_url=url, ...)\nexcept ValueError as e:\n    if \"must be https\" in str(e):\n        if os.environ.get(\"ENV\") == \"development\":\n            provider = OAuth2ClientCredentials(token_url=url, allow_insecure=True, ...)\n        else:\n            raise\n    else:\n        raise","preventionTips":["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"],"tags":["oauth2","security","tls","configuration"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}