PrefectHQ/fastmcp · error · ValueError
Assertion exp too far in future (max {self.MAX_ASSERTION_LIF
Error message
Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s) What it means
Raised when no 'iat' claim is present and the assertion's 'exp' lies further than MAX_ASSERTION_LIFETIME beyond the current server time. This is the fallback lifetime cap for assertions lacking iat — the server bounds validity from 'now' instead of from issuance.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:612
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")
# Check if JTI was already used (and hasn't expired from cache)
if jti in self._jti_cache:
cached_exp = self._jti_cache[jti]
if cached_exp > now: # Still valid in cache
raise ValueError(f"Assertion replay detected: jti {jti} already used")
# Expired in cache, can be reused (clean it up)View on GitHub (pinned to 1f02114297)
Solutions
- Shorten exp so it is within MAX_ASSERTION_LIFETIME of now
- Add a correct 'iat' claim (now) so lifetime is measured from issuance and you can use the full window legitimately
- Regenerate assertions per request with short exps
Example fix
// before
payload = {"exp": now + 86400} # no iat, huge exp
// after
payload = {"iat": now, "exp": now + 300} Defensive patterns
Strategy: validation
Validate before calling
import time
claims = jwt.decode(token, options={"verify_signature": False})
if "iat" not in claims and claims["exp"] > time.time() + 300:
raise ValueError("assertion without iat must keep exp within server max from now") Type guard
def no_iat_exp_bounded(claims: dict, max_lifetime: int) -> bool:
if "iat" in claims:
return True
return isinstance(claims.get("exp"), (int, float)) and \
claims["exp"] <= time.time() + max_lifetime Try / catch
try:
validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
if "exp too far in future" in str(e):
token = mint_assertion(client_id, lifetime=300, include_iat=True)
else:
raise Prevention
- Always include iat so lifetime is measured from issuance
- Keep exp within the server's max window either way
- Reuse a shared, tested assertion-minting function
When it happens
Trigger: An assertion with no iat but a distant exp (e.g. exp = now + 86400); signing tokens with only exp because an older OAuth template omitted iat.
Common situations: Templates from token libraries where iat is optional; teams deliberately omitting iat yet keeping long exp windows from previous setups.
Related errors
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Assertion must include exp claim
- Assertion iat is in the future
- Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
- Assertion sub claim must be {client_id}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/58a20f789b9c1208.
Report an issue: GitHub.