PrefectHQ/fastmcp · error · ValueError

Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS

Error message

Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASSERTION_LIFETIME}s)

What it means

Raised by validate_assertion when the span between 'iat' and 'exp' exceeds the validator's MAX_ASSERTION_LIFETIME. The library caps how long a single client assertion may remain valid to limit replay exposure.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:606

        # 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:
            raise ValueError("Assertion must include jti claim")

View on GitHub (pinned to 1f02114297)

Solutions

  1. Reduce the assertion lifetime: set exp = iat + a short value within MAX_ASSERTION_LIFETIME
  2. Check the validator's configured MAX_ASSERTION_LIFETIME and match it
  3. Mint per-request assertions with ~5-minute lifetimes instead of hour-long ones

Example fix

// before
payload["exp"] = payload["iat"] + 3600
// after
payload["exp"] = payload["iat"] + 300
Defensive patterns

Strategy: validation

Validate before calling

import time
MAX_LIFETIME = 300
claims = jwt.decode(token, options={"verify_signature": False})
if claims.get("iat") and claims["exp"] - claims["iat"] > MAX_LIFETIME:
    raise ValueError("assertion lifetime exceeds server cap; shorten exp")

Type guard

def lifetime_ok(claims: dict, max_lifetime: int) -> bool:
    iat, exp = claims.get("iat"), claims.get("exp")
    return not (isinstance(iat, (int, float)) and isinstance(exp, (int, float))) or \
        exp - iat <= max_lifetime

Try / catch

try:
    validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
    if "lifetime too long" in str(e):
        token = mint_assertion(client_id, lifetime=300)
    else:
        raise

Prevention

When it happens

Trigger: Minting an assertion with exp - iat greater than MAX_ASSERTION_LIFETIME (e.g. setting exp = now + 3600 when the server allows far less); reusing an organization-wide template assertion with a 1-hour lifetime.

Common situations: Copy-pasted JWT-minting code from other OAuth providers with different (longer) lifetime rules; hardcoding exp offsets that were valid against other servers.

Related errors


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