PrefectHQ/fastmcp · error · ValueError
Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
Error message
Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASSERTION_LIFETIME}s) What it means
Raised by validate_assertion when the span between 'iat' and 'exp' exceeds the validator's MAX_ASSERTION_LIFETIME. The library caps how long a single client assertion may remain valid to limit replay exposure.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:606
# 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)"
)
# 4. Additional RFC 7523 validation: sub claim must equal client_id
if claims.get("sub") != client_id:
raise ValueError(f"Assertion sub claim must be {client_id}")
# 5. Check jti for replay attacks (RFC 7523 requirement)
jti = claims.get("jti")
if not jti:
raise ValueError("Assertion must include jti claim")
View on GitHub (pinned to 1f02114297)
Solutions
- Reduce the assertion lifetime: set exp = iat + a short value within MAX_ASSERTION_LIFETIME
- Check the validator's configured MAX_ASSERTION_LIFETIME and match it
- Mint per-request assertions with ~5-minute lifetimes instead of hour-long ones
Example fix
// before payload["exp"] = payload["iat"] + 3600 // after payload["exp"] = payload["iat"] + 300
Defensive patterns
Strategy: validation
Validate before calling
import time
MAX_LIFETIME = 300
claims = jwt.decode(token, options={"verify_signature": False})
if claims.get("iat") and claims["exp"] - claims["iat"] > MAX_LIFETIME:
raise ValueError("assertion lifetime exceeds server cap; shorten exp") Type guard
def lifetime_ok(claims: dict, max_lifetime: int) -> bool:
iat, exp = claims.get("iat"), claims.get("exp")
return not (isinstance(iat, (int, float)) and isinstance(exp, (int, float))) or \
exp - iat <= max_lifetime Try / catch
try:
validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
if "lifetime too long" in str(e):
token = mint_assertion(client_id, lifetime=300)
else:
raise Prevention
- Match your assertion lifetime to the server's MAX_ASSERTION_LIFETIME
- Default to 5-minute lifetimes for client assertions
- Centralize assertion minting so lifetime policy lives in one place
When it happens
Trigger: Minting an assertion with exp - iat greater than MAX_ASSERTION_LIFETIME (e.g. setting exp = now + 3600 when the server allows far less); reusing an organization-wide template assertion with a 1-hour lifetime.
Common situations: Copy-pasted JWT-minting code from other OAuth providers with different (longer) lifetime rules; hardcoding exp offsets that were valid against other servers.
Related errors
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Assertion must include exp claim
- 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/fa03884d9f11fe08.
Report an issue: GitHub.