PrefectHQ/fastmcp · error · ValueError

CIMD document must have jwks_uri or jwks for private_key_jwt

Error message

CIMD document must have jwks_uri or jwks for private_key_jwt

What it means

validate_assertion verifies private_key_jwt client assertions against the client's keys. The CIMD document must supply those keys either as jwks_uri or inline jwks; a document with neither (and token_endpoint_auth_method implying key-based auth) makes signature verification impossible, so a ValueError is raised.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:578

                    jwks_uri=jwks_uri_str,
                    issuer=client_id,
                    audience=token_endpoint,
                    ssrf_safe=True,
                )
                if len(self._verifier_cache) >= self._verifier_cache_max_size:
                    oldest_key = next(iter(self._verifier_cache))
                    del self._verifier_cache[oldest_key]
                self._verifier_cache[cache_key] = verifier
        elif cimd_doc.jwks:
            # Inline JWKS — no caching since the key is embedded
            public_key = self._extract_public_key_from_jwks(assertion, cimd_doc.jwks)
            verifier = _JWTVerifier(
                public_key=public_key,
                issuer=client_id,
                audience=token_endpoint,
            )
        else:
            raise ValueError(
                "CIMD document must have jwks_uri or jwks for private_key_jwt"
            )

        # 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)
        access_token = await verifier.load_access_token(assertion)
        if not access_token:
            raise ValueError("Invalid JWT assertion")

        claims = access_token.claims

        # 3. Validate assertion lifetime (exp and iat)
        now = time.time()
        exp = claims.get("exp")
        iat = claims.get("iat")

        if not exp:
            raise ValueError("Assertion must include exp claim")

View on GitHub (pinned to 1f02114297)

Solutions

  1. Update the CIMD document to include either jwks_uri (public HTTPS JWKS URL) or inline jwks
  2. Ensure token_endpoint_auth_method in the document is 'private_key_jwt' if the client signs assertions
  3. If the client truly uses no auth ('none'), stop sending private_key_jwt assertions from the client
  4. Re-fetch the document — the server may have a cached older version without the keys

Example fix

// before
{"client_id": "https://app.example.com/client.json",
 "token_endpoint_auth_method": "private_key_jwt"}
// after
{"client_id": "https://app.example.com/client.json",
 "token_endpoint_auth_method": "private_key_jwt",
 "jwks_uri": "https://keys.app.example.com/jwks.json"}
Defensive patterns

Strategy: validation

Validate before calling

def can_do_private_key_jwt(doc: dict) -> bool:
    if doc.get("token_endpoint_auth_method") != "private_key_jwt":
        return True
    return bool(doc.get("jwks_uri") or doc.get("jwks"))

Type guard

def has_key_material(doc: object) -> bool:
    return isinstance(doc, dict) and bool(doc.get("jwks_uri") or doc.get("jwks"))

Try / catch

try:
    await manager.validate_private_key_jwt(doc, assertion, token_endpoint)
except ValueError as e:
    if "jwks_uri or jwks" in str(e):
        raise InvalidClientError("client document lacks key material") from e
    raise

Prevention

When it happens

Trigger: validate_private_key_jwt / validate_assertion invoked with a CIMDDocument whose jwks_uri and jwks are both None — typically a document with token_endpoint_auth_method 'none' being used for private_key_jwt, or a malformed document.

Common situations: Client documents declaring token_endpoint_auth_method='none' while the client actually sends private_key_jwt assertions; metadata documents missing key material fields entirely; key rotation scripts that removed jwks fields.

Related errors


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