PrefectHQ/fastmcp · error · IdentityAssertionError

OIDC discovery for issuer {issuer!r} failed: {e}

Error message

OIDC discovery for issuer {issuer!r} failed: {e}

What it means

FastMCP's identity assertion provider fetches the OIDC discovery document for a trusted issuer and wraps any httpx transport/HTTP error or JSON parse failure into IdentityAssertionError. This maps the failure to an OAuth invalid_grant response instead of a 500, and records a failure timestamp so repeated attempts are throttled.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:288

                failed_at is not None
                and time.monotonic() - failed_at < self._discovery_failure_cooldown
            ):
                raise IdentityAssertionError(
                    f"OIDC discovery for issuer {issuer!r} recently failed; backing off"
                )
            return await self._fetch_discovery(issuer)

    async def _fetch_discovery(self, issuer: str) -> str:
        """Perform the actual discovery fetch; caller holds the issuer lock."""
        config_url = issuer.rstrip("/") + "/.well-known/openid-configuration"
        try:
            async with httpx2.AsyncClient() as client:
                response = await client.get(config_url, timeout=10.0)
                response.raise_for_status()
                body = response.json()
        except (httpx2.HTTPError, ValueError) as e:
            self._discovery_failures[issuer] = time.monotonic()
            raise IdentityAssertionError(
                f"OIDC discovery for issuer {issuer!r} failed: {e}"
            ) from e
        if not isinstance(body, dict):
            # Valid JSON that isn't an object (e.g. `[]` or a bare string) —
            # guard before .get() so a misbehaving discovery endpoint maps to
            # invalid_grant, not a 500 on every subsequent exchange.
            raise IdentityAssertionError(
                f"OIDC discovery document for issuer {issuer!r} is not a JSON object"
            )

        jwks_uri = body.get("jwks_uri")
        if not jwks_uri or not isinstance(jwks_uri, str):
            raise IdentityAssertionError(
                f"OIDC discovery document for issuer {issuer!r} has no jwks_uri"
            )
        return jwks_uri

    async def _get_verifier(self, issuer: str) -> JWTVerifier:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Verify the issuer URL in trusted_issuers is exactly the IdP's issuer identifier and that <issuer>/.well-known/openid-configuration (or .well-known/oauth-authorization-server) resolves and returns 200 with JSON from the server host (curl it).
  2. Check network egress/DNS from the FastMCP server (proxies, firewalls, VPC rules).
  3. Check the IdP status; if it is temporarily down, retry after the recorded discovery-failure backoff window elapses.
  4. If the discovery endpoint cannot return JSON, pin the JWKS URI via explicit provider configuration instead of relying on discovery.

Example fix

// before
config = IdentityAssertionConfig(trusted_issuers={"https://idp.example.com/auth"})
// after (verify the discovery URL returns JSON first, or pin jwks_uri)
config = IdentityAssertionConfig(
    trusted_issuers={"https://idp.example.com"},  # matches IdP issuer exactly
    # or supply explicit jwks_uri so discovery is not needed
)
Defensive patterns

Strategy: retry

Validate before calling

import httpx
url = issuer.rstrip('/') + '/.well-known/openid-configuration'
async with httpx.AsyncClient() as c:
    r = await c.get(url, timeout=10.0)
    r.raise_for_status()
    assert isinstance(r.json(), dict)

Type guard

def is_valid_discovery(body) -> bool:
    return isinstance(body, dict) and isinstance(body.get('jwks_uri'), str) and bool(body['jwks_uri'])

Try / catch

from fastmcp.server.auth.identity_assertion import IdentityAssertionError
try:
    await provider.validate(assertion)
except IdentityAssertionError as e:
    if 'discovery' in str(e):
        # transient IdP/network issue: back off and retry later
        await asyncio.sleep(backoff)
    else:
        raise

Prevention

When it happens

Trigger: Calling validate() on an id-jag assertion whose issuer's discovery URL (issuer + well-known path) is unreachable, returns a non-2xx status (raise_for_status), or returns a body that is not valid JSON (response.json() raises ValueError).

Common situations: Misconfigured trusted issuer URL (typo, http vs https, missing path); IdP downtime or firewall egress blocking the server's outbound request to the IdP; discovery endpoint returning HTML error pages instead of JSON; DNS failures in containerized deployments.

Related errors


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