PrefectHQ/fastmcp · error · ValueError
Assertion replay detected: jti {jti} already used
Error message
Assertion replay detected: jti {jti} already used What it means
Raised by validate_assertion when the assertion's jti is found in the validator's replay cache and its cached exp is still in the future — meaning this exact assertion (same JWT ID) was already accepted and is being replayed. The server rejects duplicates until the cached entry expires.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:629
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)
del self._jti_cache[jti]
# Emergency size limit (shouldn't hit with proper TTL cleanup)
if len(self._jti_cache) >= self._jti_cache_max_size:
self._cleanup_expired_jtis()
# If still over limit after cleanup, reject to prevent DoS
if len(self._jti_cache) >= self._jti_cache_max_size:
self.logger.warning(
"JTI cache at max capacity (%d), possible attack",
self._jti_cache_max_size,
)
raise ValueError("Server overloaded, please retry")
# Add to cache with expiration time
# Use the assertion's exp claim so it stays cached until it would expire anyway
self._jti_cache[jti] = exp
View on GitHub (pinned to 1f02114297)
Solutions
- Mint a fresh assertion (new jti, new iat/exp) for every request, including retries
- Fix deterministic jti generation — use uuid4 or a random component per mint
- Ensure retry middleware re-invokes the token-minting callback, not the cached body
Example fix
// before assertion = build_jwt(jti="fixed-id") # reused every request // after assertion = build_jwt(jti=str(uuid.uuid4())) # fresh per request
Defensive patterns
Strategy: validation
Validate before calling
import time, uuid
# Re-mint whenever the cached assertion is near expiry or already sent once
def get_fresh_assertion(client_id, minted_state={"jti": None, "exp": 0}):
if time.time() > minted_state["exp"] - 5:
minted_state["jti"] = str(uuid.uuid4())
minted_state["exp"] = time.time() + 300
return sign_assertion(client_id, jti=minted_state["jti"]) Type guard
def is_unsent(jti: str, sent_jtis: set) -> bool:
return jti not in sent_jtis Try / catch
try:
validator.validate_assertion(token, client_id, jwks)
except ValueError as e:
if "replay detected" in str(e):
token = mint_fresh_assertion(client_id) # never resend the same jti
validator.validate_assertion(token, client_id, jwks)
else:
raise Prevention
- Mint a new assertion (new jti) for every request AND every retry
- Disable retry middleware reuse of the same serialized body
- Use uuid4, not deterministic hashes, for jti
- Never replay test fixture assertions across runs within their validity window
When it happens
Trigger: Retrying an HTTP token request with the same serialized assertion after a timeout; a client bug that mints jti deterministically (e.g. fixed string or hash of client_id) producing collisions; intercepting/replaying a captured assertion within its validity window.
Common situations: HTTP clients auto-retrying failed requests without re-minting the JWT; load balancers replaying a request body; test suites reusing a fixture token across test cases.
Related errors
- Assertion replay detected: jti {jti} reused
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
- Missing client_assertion
- Invalid client assertion: {e}
- {str(e) from SSRFError/SSRFFetchError}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/54051545951977ae.
Report an issue: GitHub.