{"record":{"id":"152c1dd341d57522","repo":"PrefectHQ/fastmcp","slug":"obo-token-exchange-requires-either-a-client-secret","errorCode":null,"errorMessage":"OBO token exchange requires either a client_secret or a subclass that overrides get_obo_credential() to provide alternative credentials (e.g., client_assertion_func for managed identity).","messagePattern":"OBO token exchange requires either a client_secret or a subclass that overrides get_obo_credential\\(\\) to provide alternative credentials \\(e\\.g\\., client_assertion_func for managed identity\\)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/providers/azure.py","lineNumber":671,"sourceCode":"\n        key = hashlib.sha256(user_assertion.encode()).hexdigest()\n\n        if key in self._obo_credentials:\n            self._obo_credentials.move_to_end(key)\n            return self._obo_credentials[key]\n\n        obo_kwargs: dict[str, Any] = {\n            \"tenant_id\": self._tenant_id,\n            \"client_id\": self._upstream_client_id,\n            \"user_assertion\": user_assertion,\n            \"authority\": f\"https://{self._base_authority}\",\n        }\n        if self._upstream_client_secret is not None:\n            obo_kwargs[\"client_secret\"] = (\n                self._upstream_client_secret.get_secret_value()\n            )\n        else:\n            raise ValueError(\n                \"OBO token exchange requires either a client_secret or a subclass \"\n                \"that overrides get_obo_credential() to provide alternative credentials \"\n                \"(e.g., client_assertion_func for managed identity).\"\n            )\n        credential = OnBehalfOfCredential(**obo_kwargs)\n        self._obo_credentials[key] = credential\n\n        # Evict oldest if over capacity\n        while len(self._obo_credentials) > self._obo_max_credentials:\n            _, evicted = self._obo_credentials.popitem(last=False)\n            await evicted.close()\n\n        return credential\n\n    async def close_obo_credentials(self) -> None:\n        \"\"\"Close all cached OBO credentials.\"\"\"\n        credentials = list(self._obo_credentials.values())\n        self._obo_credentials.clear()","sourceCodeStart":653,"sourceCodeEnd":689,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/providers/azure.py#L653-L689","documentation":"The OBO flow requires a confidential client credential to authorize the exchange. AzureProvider's get_obo_credential builds OnBehalfOfCredential kwargs; if no upstream client_secret was configured (and no subclass supplies alternative credentials like a client assertion), the exchange cannot be authenticated, so the method raises ValueError.","triggerScenarios":"Calling get_obo_credential() on an AzureProvider (Entra, OBO-capable) constructed without an upstream client secret — e.g. AzureProvider created with public-client settings or with the upstream client secret omitted.","commonSituations":"Setting up OBO in an environment that intentionally avoids stored secrets and forgetting to supply client_assertion_func via a subclass; constructing AzureProvider from a config file that omits the client secret.","solutions":["Configure the upstream client secret on the AzureProvider so _upstream_client_secret is set.","Subclass AzureProvider and override get_obo_credential() to supply alternative credentials such as client_assertion_func (certificate or managed-identity-based assertion).","If no secret can be stored, use certificate credential via azure-identity's OnBehalfOfCredential(client_certificate=...) in your override."],"exampleFix":"// before\nprovider = AzureProvider(client_id=cid, tenant_id=tid)  # no secret -> OBO fails\n// after\nclass MyProvider(AzureProvider):\n    async def get_obo_credential(self, *, user_assertion, scopes):\n        ...  # supply client_assertion_func-based OnBehalfOfCredential\nprovider = MyProvider(client_id=cid, tenant_id=tid, client_secret=secret)","handlingStrategy":"validation","validationCode":"if getattr(provider, \"_upstream_client_secret\", None) is None and type(provider).get_obo_credential is AzureProvider.get_obo_credential:\n    raise ValueError(\"Configure client_secret or override get_obo_credential before OBO\")","typeGuard":"def can_do_obo(provider) -> bool:\n    return getattr(provider, \"_upstream_client_secret\", None) is not None or type(provider).get_obo_credential is not AzureProvider.get_obo_credential","tryCatchPattern":"try:\n    cred = await provider.get_obo_credential(user_assertion=token)\nexcept ValueError as e:\n    if \"client_secret\" in str(e):\n        logger.error(\"Configure upstream client secret or a get_obo_credential override\")\n    raise","preventionTips":["Wire the upstream client secret via secret manager at provider construction","Prefer subclass override with client_assertion_func where secrets can't be stored","Fail fast at startup by probing OBO readiness before serving traffic"],"tags":["python","azure","obo","credentials","configuration"],"backgroundTag":"missing-client-secret","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}