PrefectHQ/fastmcp · warning · IdentityAssertionError

OIDC discovery for issuer {issuer!r} recently failed; backin

Error message

OIDC discovery for issuer {issuer!r} recently failed; backing off

What it means

OIDC discovery (.well-known/openid-configuration) for a trusted issuer failed recently, and the manager enforces a cooldown per issuer before retrying. Within that window `_discover_jwks_uri` raises `IdentityAssertionError` immediately instead of re-fetching, protecting the authorization server from repeated slow/timing-out discovery requests.

Source

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

            self._cleanup_expired_jtis()
            self._last_cleanup = now

    async def _discover_jwks_uri(self, issuer: str) -> str:
        """Discover an issuer's JWKS URI via OIDC discovery.

        Fetches ``{issuer}/.well-known/openid-configuration`` and returns its
        ``jwks_uri``. Trusted issuers are operator-configured, so this uses a
        plain fetch (consistent with how operator-configured JWKS URIs are
        treated elsewhere, including localhost issuers in development).
        """
        lock = self._discovery_locks.setdefault(issuer, asyncio.Lock())
        async with lock:
            failed_at = self._discovery_failures.get(issuer)
            if (
                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):

View on GitHub (pinned to 1f02114297)

Solutions

  1. Wait for the cooldown window to pass and retry — discovery will be attempted again automatically
  2. Verify the issuer URL is correct and that `https://<issuer>/.well-known/openid-configuration` is reachable from the server
  3. Fix network egress/DNS/TLS issues between your server and the issuer (proxy settings, firewall rules)
  4. If the issuer has no discovery document, configure its JWKS URI statically instead of relying on discovery

Example fix

// before (issuer unreachable / typo)
trusted_issuers=["https://issuer.example.co"]
// after
type: curl https://issuer.example.com/.well-known/openid-configuration  # verify then use
trusted_issuers=["https://issuer.example.com"]
Defensive patterns

Strategy: retry

Validate before calling

import httpx
r = httpx.get(f"{issuer.rstrip('/')}/.well-known/openid-configuration", timeout=5)
r.raise_for_status()
assert "jwks_uri" in r.json(), "discovery doc missing jwks_uri"

Type guard

def issuer_discoverable(issuer: str) -> bool:
    try:
        r = httpx.get(f"{issuer.rstrip('/')}/.well-known/openid-configuration", timeout=5)
        return r.ok and "jwks_uri" in r.json()
    except httpx.HTTPError:
        return False

Try / catch

try:
    verifier = await manager._get_verifier(issuer)
except IdentityAssertionError as e:
    if "backing off" in str(e):
        await asyncio.sleep(manager._discovery_failure_cooldown)
        verifier = await manager._get_verifier(issuer)
    else:
        raise

Prevention

When it happens

Trigger: `_get_verifier` (via `_discover_jwks_uri`) is called for an issuer whose previous discovery fetch failed within `_discovery_failure_cooldown` seconds — e.g. the issuer's discovery endpoint was down or timed out moments earlier and another token exchange arrives.

Common situations: Issuer's well-known endpoint temporarily unreachable (network outage, DNS failure, TLS problems); issuer misconfigured in trusted_issuers (wrong URL, no discovery document); rate limiting or firewall blocking the server's outbound requests; flaky issuer during a deploy window.

Related errors


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