{"record":{"id":"ccf5f9463c23710d","repo":"headroomlabs-ai/headroom","slug":"client-id-and-client-secret-are-required","errorCode":null,"errorMessage":"client_id and client_secret are required","messagePattern":"client_id and client_secret are required","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"plugins/headroom-oauth2/src/headroom_oauth2/provider.py","lineNumber":60,"sourceCode":"    def __init__(\n        self,\n        *,\n        token_url: str,\n        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()","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/plugins/headroom-oauth2/src/headroom_oauth2/provider.py#L42-L78","documentation":"OAuth2ClientCredentials requires both `client_id` and `client_secret` to be non-empty at construction; missing either raises ValueError. Client-credentials flow authenticates the application itself with this pair, so there is nothing sensible to default.","triggerScenarios":"Constructing the provider with an empty/None `client_id` or `client_secret` — typically values read from env vars (`HEADROOM_OAUTH2_CLIENT_ID`/`..._CLIENT_SECRET`) that are unset in the current shell, pod, or CI runner while TOKEN_URL is set.","commonSituations":"Secrets not injected into the deployment (Kubernetes secret missing, CI secret not exposed, .env not loaded); secret name typos; running locally with prod config that expects a vault; empty-string values from templating (`CLIENT_ID=${OAUTH_CLIENT_ID}` when the inner var is unset).","solutions":["Set both credentials in the environment/config the provider reads from, and verify they are non-empty before constructing (see validation code)","Check secret injection in the deployment: `kubectl describe pod`, CI masked-variable exposure, or print `bool(os.environ.get(...))` (never the value) in a startup probe","If OAuth2 shouldn't be active in this environment, unset HEADROOM_OAUTH2_TOKEN_URL so the provider is never built"],"exampleFix":"# before\nprovider = OAuth2ClientCredentials(\n    token_url=os.environ[\"HEADROOM_OAUTH2_TOKEN_URL\"],\n    client_id=os.environ.get(\"HEADROOM_OAUTH2_CLIENT_ID\", \"\"),\n    client_secret=os.environ.get(\"HEADROOM_OAUTH2_CLIENT_SECRET\", \"\"),\n)\n\n# after\nmissing = [k for k in (\"HEADROOM_OAUTH2_CLIENT_ID\", \"HEADROOM_OAUTH2_CLIENT_SECRET\")\n           if not os.environ.get(k, \"\").strip()]\nif missing:\n    raise RuntimeError(f\"missing oauth2 config: {missing}\")\nprovider = OAuth2ClientCredentials(\n    token_url=os.environ[\"HEADROOM_OAUTH2_TOKEN_URL\"],\n    client_id=os.environ[\"HEADROOM_OAUTH2_CLIENT_ID\"],\n    client_secret=os.environ[\"HEADROOM_OAUTH2_CLIENT_SECRET\"],\n)","handlingStrategy":"validation","validationCode":"REQUIRED = (\"HEADROOM_OAUTH2_TOKEN_URL\", \"HEADROOM_OAUTH2_CLIENT_ID\", \"HEADROOM_OAUTH2_CLIENT_SECRET\")\n\ndef oauth2_env_complete(env) -> None:\n    missing = [k for k in REQUIRED if not str(env.get(k) or \"\").strip()]\n    if missing:\n        raise RuntimeError(f\"oauth2 env incomplete, missing: {missing}\")\n\noauth2_env_complete(os.environ)\nprovider = provider_from_env()","typeGuard":"def credentials_present(client_id, client_secret) -> bool:\n    return bool(str(client_id or \"\").strip()) and bool(str(client_secret or \"\").strip())","tryCatchPattern":"try:\n    provider = OAuth2ClientCredentials(token_url=url, client_id=cid, client_secret=sec)\nexcept ValueError as e:\n    if \"client_id and client_secret are required\" in str(e):\n        raise RuntimeError(\"deployment error: oauth2 secrets not injected\") from e\n    raise","preventionTips":["Startup probe: assert required env vars are non-empty (never print their values)","Watch for empty-string templating: `${VAR}` unset in shell yields empty, not missing","Confirm secret injection in k8s/CI before first deploy of the plugin"],"tags":["oauth2","validation","secrets","configuration"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}