PrefectHQ/fastmcp · error · ValueError
Assertion has expired
Error message
Assertion has expired
What it means
Raised by validate_assertion when the assertion's 'exp' claim is older than the current time minus a 30-second clock-skew allowance, i.e. the JWT has expired. The library enforces assertion freshness per RFC 7523 so replayed stale assertions are rejected.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:599
# 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)
access_token = await verifier.load_access_token(assertion)
if not access_token:
raise ValueError("Invalid JWT assertion")
claims = access_token.claims
# 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:View on GitHub (pinned to 1f02114297)
Solutions
- Mint a fresh assertion at request time (exp = now + 300) instead of caching it
- Synchronize the machine clock (NTP) if drift is the cause
- Increase the assertion's exp window slightly if your flow legitimately needs longer validity (respecting the server's MAX_ASSERTION_LIFETIME)
Example fix
// before exp = iat + 3600 # long-lived assertion gets cached and reused // after exp = int(time.time()) + 300 # mint fresh per request
Defensive patterns
Strategy: validation
Validate before calling
import time
claims = jwt.decode(token, options={"verify_signature": False})
if claims.get("exp", 0) <= time.time():
token = mint_fresh_assertion(client_id) # re-mint before calling Type guard
def is_currently_valid(claims: dict, skew: float = 30) -> bool:
return isinstance(claims.get("exp"), (int, float)) and claims["exp"] > time.time() - skew Try / catch
try:
validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
if "expired" in str(e):
token = mint_fresh_assertion(client_id)
validator.validate_assertion(token, client_id, jwks)
else:
raise Prevention
- Mint assertions per-request instead of caching them
- Run NTP on all hosts minting/validating tokens
- Keep assertion lifetimes short (~5 minutes)
When it happens
Trigger: Presenting a cached or long-lived client assertion after its exp timestamp passed; a machine clock set more than 30 seconds behind the server's; reusing a persisted token file from a previous session.
Common situations: Clients caching the signed assertion instead of minting a fresh one per token request; VMs/containers with clock drift; slow requests where a short (e.g. 60s) exp lapsed before validation.
Related errors
- Assertion iat is in the future
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
- Missing client_assertion
- Invalid client assertion: {e}
- CIMD document must have jwks_uri or jwks for private_key_jwt
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/21b762a51f081195.
Report an issue: GitHub.