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

  1. Shorten exp so it is within MAX_ASSERTION_LIFETIME of now
  2. Add a correct 'iat' claim (now) so lifetime is measured from issuance and you can use the full window legitimately
  3. 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

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


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