PrefectHQ/fastmcp · error · ValueError

OBO token exchange requires either a client_secret or a subc

Error message

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).

What it means

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.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/providers/azure.py:671

        key = hashlib.sha256(user_assertion.encode()).hexdigest()

        if key in self._obo_credentials:
            self._obo_credentials.move_to_end(key)
            return self._obo_credentials[key]

        obo_kwargs: dict[str, Any] = {
            "tenant_id": self._tenant_id,
            "client_id": self._upstream_client_id,
            "user_assertion": user_assertion,
            "authority": f"https://{self._base_authority}",
        }
        if self._upstream_client_secret is not None:
            obo_kwargs["client_secret"] = (
                self._upstream_client_secret.get_secret_value()
            )
        else:
            raise ValueError(
                "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)."
            )
        credential = OnBehalfOfCredential(**obo_kwargs)
        self._obo_credentials[key] = credential

        # Evict oldest if over capacity
        while len(self._obo_credentials) > self._obo_max_credentials:
            _, evicted = self._obo_credentials.popitem(last=False)
            await evicted.close()

        return credential

    async def close_obo_credentials(self) -> None:
        """Close all cached OBO credentials."""
        credentials = list(self._obo_credentials.values())
        self._obo_credentials.clear()

View on GitHub (pinned to 1f02114297)

Solutions

  1. Configure the upstream client secret on the AzureProvider so _upstream_client_secret is set.
  2. Subclass AzureProvider and override get_obo_credential() to supply alternative credentials such as client_assertion_func (certificate or managed-identity-based assertion).
  3. If no secret can be stored, use certificate credential via azure-identity's OnBehalfOfCredential(client_certificate=...) in your override.

Example fix

// before
provider = AzureProvider(client_id=cid, tenant_id=tid)  # no secret -> OBO fails
// after
class MyProvider(AzureProvider):
    async def get_obo_credential(self, *, user_assertion, scopes):
        ...  # supply client_assertion_func-based OnBehalfOfCredential
provider = MyProvider(client_id=cid, tenant_id=tid, client_secret=secret)
Defensive patterns

Strategy: validation

Validate before calling

if getattr(provider, "_upstream_client_secret", None) is None and type(provider).get_obo_credential is AzureProvider.get_obo_credential:
    raise ValueError("Configure client_secret or override get_obo_credential before OBO")

Type guard

def can_do_obo(provider) -> bool:
    return getattr(provider, "_upstream_client_secret", None) is not None or type(provider).get_obo_credential is not AzureProvider.get_obo_credential

Try / catch

try:
    cred = await provider.get_obo_credential(user_assertion=token)
except ValueError as e:
    if "client_secret" in str(e):
        logger.error("Configure upstream client secret or a get_obo_credential override")
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/152c1dd341d57522. Report an issue: GitHub.