PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion iat is in the future
Error message
Assertion iat is in the future
What it means
The assertion JWT's `iat` (issued-at) claim is in the future by more than `CLOCK_SKEW_SECONDS` beyond the server's clock. The middleware rejects assertions whose issue time hasn't occurred yet, guarding against forged or badly timestamped tokens (RFC 7523 sanity requirements).
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:394
verifier = await self._get_verifier(iss)
access_token = await verifier.load_access_token(assertion)
if access_token is None:
raise IdentityAssertionError(
"Assertion failed signature/issuer/audience/expiry validation"
)
claims = access_token.claims
now = time.time()
exp = _numeric_date_claim(claims, "exp")
iat = _numeric_date_claim(claims, "iat")
nbf = _numeric_date_claim(claims, "nbf")
if exp is None:
raise IdentityAssertionError("Assertion must include exp claim")
if nbf is not None and nbf > now + self.CLOCK_SKEW_SECONDS:
raise IdentityAssertionError("Assertion is not yet valid (nbf in future)")
if iat is not None:
if iat > now + self.CLOCK_SKEW_SECONDS:
raise IdentityAssertionError("Assertion iat is in the future")
if exp - iat > self.MAX_ASSERTION_LIFETIME:
raise IdentityAssertionError(
f"Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s)"
)
elif exp > now + self.MAX_ASSERTION_LIFETIME:
raise IdentityAssertionError(
f"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)"
)
# 4. sub is mandatory (RFC 7523 §3) — it identifies the end user.
sub = claims.get("sub")
if not sub:
raise IdentityAssertionError("Assertion must include sub claim")
# 5. Required scopes on the issued access token derive from the assertion.
if self.config.required_scopes:
granted = set(_assertion_scopes(claims))
missing = set(self.config.required_scopes) - grantedView on GitHub (pinned to 1f02114297)
Solutions
- Synchronize the assertion-issuing machine's clock (NTP) with the server.
- Ensure the issuer sets `iat = int(time.time())` at signing time, not a scheduled/offset time.
- Confirm iat is epoch seconds; if the value is ~13 digits, convert from milliseconds.
- If legitimate skew is small and recurring, the server operator can increase `CLOCK_SKEW_SECONDS` in the config.
Example fix
// before
claims = {"iat": int(datetime(2026, 9, 1, tzinfo=timezone.utc).timestamp()), "exp": now + 300}
// after
claims = {"iat": int(time.time()), "exp": int(time.time()) + 300} Defensive patterns
Strategy: validation
Validate before calling
import time
def iat_is_valid(claims: dict, skew: float = 60) -> bool:
iat = claims.get("iat")
return iat is None or (isinstance(iat, (int, float)) and iat <= time.time() + skew) Type guard
def is_epoch_seconds(v) -> bool:
return isinstance(v, (int, float)) and not isinstance(v, bool) and v < 10_000_000_000 Try / catch
try:
token = await exchange(assertion)
except IdentityAssertionError as e:
if "iat is in the future" in str(e):
assertion = mint_assertion() # re-mint with server-consistent timestamp
token = await exchange(assertion)
else:
raise Prevention
- Generate iat at signing time with int(time.time()) in UTC.
- Keep minting machines NTP-synchronized with the server.
- Avoid timezone-aware date math when computing iat; use epoch helpers.
When it happens
Trigger: Calling `validate()` with an assertion where `iat > now + CLOCK_SKEW_SECONDS` — client machine clock set ahead, iat accidentally generated from a wrong timezone-affected date computation, or iat encoded in milliseconds (huge numeric value interpreted as far-future).
Common situations: Developer laptop with drifted clock minting assertions locally; custom minting code computing iat via `datetime.now(timezone.utc).timestamp()` on a machine with wrong date; ms/seconds unit mixups.
Related errors
- Assertion is not yet valid (nbf in future)
- Assertion must include exp claim
- Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
- Assertion exp too far in future (max {self.MAX_ASSERTION_LIF
- Assertion must include sub claim
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/44aeb95dbf34d819.
Report an issue: GitHub.