{"record":{"id":"f4650b18906e974f","repo":"PrefectHQ/fastmcp","slug":"oidc-discovery-for-issuer-issuer-r-failed-e","errorCode":null,"errorMessage":"OIDC discovery for issuer {issuer!r} failed: {e}","messagePattern":"OIDC discovery for issuer (.+?) failed: (.+?)","errorType":"exception","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":288,"sourceCode":"                failed_at is not None\n                and time.monotonic() - failed_at < self._discovery_failure_cooldown\n            ):\n                raise IdentityAssertionError(\n                    f\"OIDC discovery for issuer {issuer!r} recently failed; backing off\"\n                )\n            return await self._fetch_discovery(issuer)\n\n    async def _fetch_discovery(self, issuer: str) -> str:\n        \"\"\"Perform the actual discovery fetch; caller holds the issuer lock.\"\"\"\n        config_url = issuer.rstrip(\"/\") + \"/.well-known/openid-configuration\"\n        try:\n            async with httpx2.AsyncClient() as client:\n                response = await client.get(config_url, timeout=10.0)\n                response.raise_for_status()\n                body = response.json()\n        except (httpx2.HTTPError, ValueError) as e:\n            self._discovery_failures[issuer] = time.monotonic()\n            raise IdentityAssertionError(\n                f\"OIDC discovery for issuer {issuer!r} failed: {e}\"\n            ) from e\n        if not isinstance(body, dict):\n            # Valid JSON that isn't an object (e.g. `[]` or a bare string) —\n            # guard before .get() so a misbehaving discovery endpoint maps to\n            # invalid_grant, not a 500 on every subsequent exchange.\n            raise IdentityAssertionError(\n                f\"OIDC discovery document for issuer {issuer!r} is not a JSON object\"\n            )\n\n        jwks_uri = body.get(\"jwks_uri\")\n        if not jwks_uri or not isinstance(jwks_uri, str):\n            raise IdentityAssertionError(\n                f\"OIDC discovery document for issuer {issuer!r} has no jwks_uri\"\n            )\n        return jwks_uri\n\n    async def _get_verifier(self, issuer: str) -> JWTVerifier:","sourceCodeStart":270,"sourceCodeEnd":306,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L270-L306","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["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).","Check network egress/DNS from the FastMCP server (proxies, firewalls, VPC rules).","Check the IdP status; if it is temporarily down, retry after the recorded discovery-failure backoff window elapses.","If the discovery endpoint cannot return JSON, pin the JWKS URI via explicit provider configuration instead of relying on discovery."],"exampleFix":"// before\nconfig = IdentityAssertionConfig(trusted_issuers={\"https://idp.example.com/auth\"})\n// after (verify the discovery URL returns JSON first, or pin jwks_uri)\nconfig = IdentityAssertionConfig(\n    trusted_issuers={\"https://idp.example.com\"},  # matches IdP issuer exactly\n    # or supply explicit jwks_uri so discovery is not needed\n)","handlingStrategy":"retry","validationCode":"import httpx\nurl = issuer.rstrip('/') + '/.well-known/openid-configuration'\nasync with httpx.AsyncClient() as c:\n    r = await c.get(url, timeout=10.0)\n    r.raise_for_status()\n    assert isinstance(r.json(), dict)","typeGuard":"def is_valid_discovery(body) -> bool:\n    return isinstance(body, dict) and isinstance(body.get('jwks_uri'), str) and bool(body['jwks_uri'])","tryCatchPattern":"from fastmcp.server.auth.identity_assertion import IdentityAssertionError\ntry:\n    await provider.validate(assertion)\nexcept IdentityAssertionError as e:\n    if 'discovery' in str(e):\n        # transient IdP/network issue: back off and retry later\n        await asyncio.sleep(backoff)\n    else:\n        raise","preventionTips":["Curl the discovery URL from the server host during deployment smoke tests","Pin jwks_uri explicitly if discovery reliability is a concern","Monitor IdP status and configure alerting on repeated discovery failures","Use https and exact issuer identifiers in trusted_issuers"],"tags":["network","oidc","discovery","http"],"backgroundTag":"oidc-discovery-failed","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}