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

  1. Sync clocks via NTP on the assertion-signing machine
  2. Compute iat as int(time.time()) (seconds, not milliseconds) when minting
  3. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/67ad28650f6da290. Report an issue: GitHub.