PrefectHQ/fastmcp · error · ValueError
Assertion iat is in the future
Error message
Assertion iat is in the future
What it means
Raised by validate_assertion when the assertion's 'iat' (issued-at) claim is more than 30 seconds in the future relative to the server clock. A future iat means the assertion was minted on a machine whose clock runs ahead, so the server treats it as not-yet-valid/suspicious.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:604
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)"
)
# 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:View on GitHub (pinned to 1f02114297)
Solutions
- Sync clocks via NTP on the assertion-signing machine
- Compute iat as int(time.time()) (seconds, not milliseconds) when minting
- Remove any hardcoded/future-dated iat in test fixtures
Example fix
// before iat = int(time.time() * 1000) # milliseconds // after iat = int(time.time()) # seconds
Defensive patterns
Strategy: validation
Validate before calling
import time
claims = jwt.decode(token, options={"verify_signature": False})
iat = claims.get("iat")
if iat is not None and iat > time.time() + 30:
raise ValueError("iat is in the future; check clock sync / timestamp units") Type guard
def iat_not_future(claims: dict, skew: float = 30) -> bool:
iat = claims.get("iat")
return not isinstance(iat, (int, float)) or iat <= time.time() + skew Try / catch
try:
validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
if "iat is in the future" in str(e):
sync_clock_and_remint()
else:
raise Prevention
- Use int(time.time()) (seconds) for iat, never milliseconds
- Keep clocks NTP-synced across services
- Never hardcode future timestamps in fixtures
When it happens
Trigger: Token issuer host has a clock more than 30s ahead of the CIMD validator host; manual construction of iat using milliseconds instead of seconds (iat = now_ms/1000 mistake variants like int(time.time()*1000) misused).
Common situations: Distributed deployments without NTP; local dev asserting against a remote server with skewed clocks; unit tests that hardcode future timestamps.
Related errors
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Assertion must include exp claim
- Assertion has expired
- Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
- Assertion exp too far in future (max {self.MAX_ASSERTION_LIF
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/67ad28650f6da290.
Report an issue: GitHub.