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) - granted

View on GitHub (pinned to 1f02114297)

Solutions

  1. Synchronize the assertion-issuing machine's clock (NTP) with the server.
  2. Ensure the issuer sets `iat = int(time.time())` at signing time, not a scheduled/offset time.
  3. Confirm iat is epoch seconds; if the value is ~13 digits, convert from milliseconds.
  4. 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

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


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