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
- Update the CIMD document to include either jwks_uri (public HTTPS JWKS URL) or inline jwks
- Ensure token_endpoint_auth_method in the document is 'private_key_jwt' if the client signs assertions
- If the client truly uses no auth ('none'), stop sending private_key_jwt assertions from the client
- 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
- Always publish jwks_uri or inline jwks when using private_key_jwt
- Keep token_endpoint_auth_method consistent with how the client actually authenticates
- Re-fetch documents after the client updates key material
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
- Invalid JWT assertion
- Assertion must include exp claim
- Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
- Invalid client assertion: {e}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/f2b1653a5be8f286.
Report an issue: GitHub.