PrefectHQ/fastmcp · error · IdentityAssertionError
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
When the assertion has no `iat` claim, the middleware falls back to checking `exp` directly against wall-clock time: an expiry more than `MAX_ASSERTION_LIFETIME` seconds in the future is rejected. This prevents clients from skipping the iat-based lifetime cap simply by omitting iat.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:400
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
if missing:
raise IdentityAssertionError(
f"Assertion missing required scopes: {sorted(missing)}"
)
# 6. The signed client_id and resource claims bind the assertion to theView on GitHub (pinned to 1f02114297)
Solutions
- Include `iat` in the assertion and set `exp = iat + short_lifetime`.
- Shorten the exp so `exp <= now + MAX_ASSERTION_LIFETIME` (e.g. now + 300).
- If longer windows are required, the server operator can raise `MAX_ASSERTION_LIFETIME` in the config.
- Verify with a JWT decoder that exp is a near-future epoch-seconds value.
Example fix
// before
claims = {"iss": iss, "sub": sub, "aud": aud, "exp": now + 7200}
// after
claims = {"iss": iss, "sub": sub, "aud": aud, "iat": now, "exp": now + 300} Defensive patterns
Strategy: validation
Validate before calling
import time
def exp_not_too_far(claims: dict, max_lifetime: int = 300) -> bool:
exp = claims.get("exp")
return isinstance(exp, (int, float)) and exp <= time.time() + max_lifetime Type guard
def has_near_exp(claims, max_lifetime=300) -> bool:
exp = claims.get("exp")
return isinstance(exp, (int, float)) and not isinstance(exp, bool) Try / catch
try:
token = await exchange(assertion)
except IdentityAssertionError as e:
if "exp too far in future" in str(e):
assertion = mint_assertion(iat=int(time.time()), exp=int(time.time()) + 300)
token = await exchange(assertion)
else:
raise Prevention
- Always include iat so the lifetime is measured from issuance, not wall clock.
- Cap exp at now + a few minutes when minting assertions.
- Check issuer templates for long default expiries and override them.
When it happens
Trigger: Calling `validate()` with an assertion that omits `iat` but sets `exp` more than MAX_ASSERTION_LIFETIME seconds ahead of the server's now — e.g. exp set hours/days out on an iat-less token.
Common situations: Issuers that emit exp-only JWTs (common for opaque access tokens) reused as identity assertions; hand-built test JWTs with only iss/sub/aud/exp; template tokens with long default expiries.
Related errors
- Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
- Assertion must include exp claim
- Assertion is not yet valid (nbf in future)
- Assertion iat is in the future
- Assertion must include sub claim
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/4297a9220af8a93a.
Report an issue: GitHub.