PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
Error message
Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s) What it means
When the assertion carries `iat`, the middleware caps the assertion lifetime: `exp - iat` must not exceed `MAX_ASSERTION_LIFETIME` seconds. Identity assertions are meant to be short-lived, single-use credentials (RFC 7523); a long-lived assertion is rejected because it broadens the replay/theft window.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:396
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
if missing:
raise IdentityAssertionError(View on GitHub (pinned to 1f02114297)
Solutions
- Mint assertions with a short lifetime (e.g. `exp = iat + 60..300` seconds) well under MAX_ASSERTION_LIFETIME.
- If the use case genuinely needs longer windows, the server operator can raise `MAX_ASSERTION_LIFETIME` in the identity assertion config.
- Check issuer SDK defaults for token TTL and override them for the assertion endpoint.
- Decode the JWT and compute exp-iat to confirm the actual lifetime being minted.
Example fix
// before
claims = {"iat": now, "exp": now + 86400}
// after
claims = {"iat": now, "exp": now + 300} Defensive patterns
Strategy: validation
Validate before calling
def lifetime_is_short(claims: dict, max_lifetime: int = 300) -> bool:
iat, exp = claims.get("iat"), claims.get("exp")
return isinstance(iat, (int, float)) and isinstance(exp, (int, float)) and exp - iat <= max_lifetime Type guard
def has_short_lifetime(claims, max_lifetime=300) -> bool:
return isinstance(claims.get("iat"), (int, float)) and isinstance(claims.get("exp"), (int, float)) Try / catch
try:
token = await exchange(assertion)
except IdentityAssertionError as e:
if "lifetime too long" in str(e):
assertion = mint_assertion(lifetime=300)
token = await exchange(assertion)
else:
raise Prevention
- Use a minting helper with a fixed short default lifetime (e.g. 5 minutes).
- Never reuse access-token TTLs for ID-JAG assertions.
- Document the server's MAX_ASSERTION_LIFETIME with the issuer's owners.
When it happens
Trigger: Calling `validate()` with an assertion where `exp - iat > MAX_ASSERTION_LIFETIME` — e.g. an issuer minting an assertion with a 24-hour or multi-day expiry instead of a few minutes.
Common situations: Reusing an existing access-token TTL (hours) for the ID-JAG assertion; a template JWT with default long expiry adapted to assertions; confusion between access-token lifetimes and assertion lifetimes.
Related errors
- Assertion exp too far in future (max {self.MAX_ASSERTION_LIF
- 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/1a7dcde85b4b3348.
Report an issue: GitHub.