PrefectHQ/fastmcp · error · ValueError
Invalid JWT assertion
Error message
Invalid JWT assertion
What it means
validate_assertion verifies the private_key_jwt client assertion's signature, expiration, issuer, and audience via JWTVerifier. If the assertion fails any of these checks, load_access_token returns None and a ValueError('Invalid JWT assertion') is raised — the client presented a malformed, wrongly signed, expired, or wrongly-addressed JWT.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:585
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")
# Validate exp is in the future (with small clock skew tolerance)
if exp < now - 30: # 30 second clock skew tolerance
raise ValueError("Assertion has expired")
# If iat is present, validate it and check assertion lifetime
if iat:
if iat > now + 30: # 30 second clock skew toleranceView on GitHub (pinned to 1f02114297)
Solutions
- Sign the assertion with the private key corresponding to a key published in the document's jwks/jwks_uri
- Set 'iss' to the client_id and 'aud' to the FastMCP token endpoint URL exactly
- Check system clocks and keep assertion lifetime short (exp within a few minutes)
- Decode the assertion locally (without verification) to compare iss/aud/exp claims against expectations
- Catch ValueError and respond with OAuth invalid_client / invalid_grant per the token endpoint contract
Example fix
# before
claims = {"iss": "https://app.example.com", "aud": "https://api.example.com", ...}
# after
claims = {
"iss": client_id,
"aud": "https://mcp.example.com/token",
"sub": client_id,
"exp": int(time.time()) + 300,
"jti": str(uuid4()),
} Defensive patterns
Strategy: try-catch
Validate before calling
import time, jwt # pre-check on the client before sending the assertion
claims = {"iss": client_id, "sub": client_id, "aud": token_endpoint,
"exp": int(time.time()) + 300, "jti": str(uuid4())}
assertion = jwt.encode(claims, private_key, algorithm="RS256", headers={"kid": kid}) Type guard
def assertion_shape_ok(token: str) -> bool:
parts = token.split(".")
return len(parts) == 3 and all(parts) Try / catch
try:
await manager.validate_private_key_jwt(doc, assertion, token_endpoint)
except ValueError:
raise InvalidClientError("invalid client assertion") from None Prevention
- Set iss and sub to the client_id and aud to the token endpoint URL
- Keep exp short and synchronize clocks (NTP) on the client
- Rotate keys by adding new keys to the JWKS before removing old ones
- Sign with the key whose kid is published in the client's jwks/jwks_uri
When it happens
Trigger: validate_private_key_jwt called with an assertion JWT that: is signed by a key not in the client's JWKS, has a mismatched 'iss' (must equal the client_id), has 'aud' not matching the token endpoint, is expired, or is structurally malformed.
Common situations: Client signing with a rotated-out key while the JWKS still lists (or no longer lists) the right key; 'aud' set to a resource server instead of the token endpoint URL; clock skew causing exp/nbf failures; 'iss' not exactly equal to the client_id URL; libraries issuing JWS instead of a proper JWT with claims.
Related errors
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
- Missing client_assertion
- Invalid client assertion: {e}
- Assertion must include exp claim
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/e6aaf9425f42f16d.
Report an issue: GitHub.