PrefectHQ/fastmcp · error · ValueError
Assertion must include exp claim
Error message
Assertion must include exp claim
What it means
This ValueError is raised by CIMDValidator.validate_assertion when the private_key_jwt client assertion (a JWT) lacks the mandatory 'exp' (expiration) claim. RFC 7523 requires exp so the server can bound the assertion's lifetime; without it the assertion could never expire, so the library refuses it outright before any other lifetime checks.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:595
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 tolerance
raise ValueError("Assertion iat is in the future")
if exp - iat > self.MAX_ASSERTION_LIFETIME:
raise ValueError(
f"Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASSERTION_LIFETIME}s)"
)
else:
# No iat, enforce max lifetime from now
if exp > now + self.MAX_ASSERTION_LIFETIME:
raise ValueError(
f"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)"View on GitHub (pinned to 1f02114297)
Solutions
- Add an 'exp' claim (Unix timestamp, seconds) to the assertion payload, typically now + 300 for a 5-minute lifetime
- Ensure exp is a numeric (int) timestamp, not an ISO string, since the code compares exp < now - 30
- Regenerate the assertion with a standard OIDC-compliant JWT helper that always sets exp
Example fix
// before
payload = {"iss": client_id, "sub": client_id, "aud": token_endpoint, "iat": now, "jti": jti}
// after
payload = {"iss": client_id, "sub": client_id, "aud": token_endpoint, "iat": now,
"exp": now + 300, "jti": jti} Defensive patterns
Strategy: validation
Validate before calling
import time
claims = jwt.decode(assertion, options={"verify_signature": False})
if not claims.get("exp"):
raise ValueError("assertion payload must include a numeric exp claim")
if not isinstance(claims["exp"], (int, float)):
raise TypeError("exp must be a Unix timestamp") Type guard
def has_exp(claims: dict) -> bool:
exp = claims.get("exp")
return isinstance(exp, (int, float)) and exp > 0 Prevention
- Always include exp when building JWT payloads
- Use a standard OIDC assertion helper that sets exp by default
- Write a unit test asserting presence of exp, iat, jti in minted assertions
When it happens
Trigger: Calling validate_assertion (directly or via validate_private_key_jwt) with a signed JWT whose payload has no 'exp' claim — e.g. a hand-built assertion payload dict that only includes iss/sub/aud/iat/jti, or a token minted by a custom JWT helper that omits exp.
Common situations: Developers manually constructing client assertions for OAuth private_key_jwt auth and forgetting exp; migrating from a token format that didn't require exp; using a JWT library where exp is optional by default.
Related errors
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
- Invalid JWT assertion
- Assertion iat is in the future
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/f54231e4103dcefe.
Report an issue: GitHub.