PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion must include a string jti claim
Error message
Assertion must include a string jti claim
What it means
This error is raised by IdentityAssertion.validate() when validating an RFC 7523 identity/ID-JAG assertion whose JWT claims lack a usable 'jti' (JWT ID) claim. The jti must be a non-empty string because it is used as the key in the replay-detection cache; an array or object jti would be unhashable and crash the cache lookup with a TypeError/500 instead of a clean invalid_grant rejection. The library requires it per RFC 7523 §3.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:452
claim_matches = assertion_resource.rstrip("/") == resource_url.rstrip(
"/"
)
else:
claim_matches = normalize_resource_url(
assertion_resource
) == normalize_resource_url(resource_url)
if not claim_matches:
raise IdentityAssertionError(
f"Assertion resource {assertion_resource!r} does not match "
f"this server {resource_url!r}"
)
# 7. jti replay rejection (RFC 7523 §3). Must be a non-empty string —
# an array/object jti is unhashable and would raise TypeError on the
# cache lookup (a 500) instead of a clean invalid_grant.
jti = claims.get("jti")
if not jti or not isinstance(jti, str):
raise IdentityAssertionError("Assertion must include a string jti claim")
cached_exp = self._jti_cache.get(jti)
if cached_exp is not None and cached_exp > now:
raise IdentityAssertionError(f"Assertion replay detected: jti {jti} reused")
# Enforce the cap BEFORE inserting so a rejected assertion never grows the
# cache. A fresh jti that would exceed capacity is rejected outright (after
# a cleanup pass to reclaim any expired entries first).
if (
jti not in self._jti_cache
and len(self._jti_cache) >= self._jti_cache_max_size
):
self._cleanup_expired_jtis()
if len(self._jti_cache) >= self._jti_cache_max_size:
logger.warning("ID-JAG jti cache at capacity, possible attack")
raise IdentityAssertionError("Server overloaded, please retry")
self._jti_cache[jti] = exp
logger.debug("ID-JAG validated for subject=%s issuer=%s", sub, iss)View on GitHub (pinned to 1f02114297)
Solutions
- Add a unique, non-empty string 'jti' claim (e.g. a UUID) to every assertion JWT at mint time
- Verify the issuer does not serialize jti as a number/array/object — cast to str(uuid4()) before signing
- Decode the assertion locally (jwt.decode without verification) to inspect the actual claims before submitting
- If you don't need replay protection semantics, still supply jti — it is mandatory in this validator
Example fix
// before: payload built without jti
payload = {"iss": iss, "sub": sub, "aud": aud, "exp": exp}
// after
payload = {"iss": iss, "sub": sub, "aud": aud, "exp": exp, "jti": str(uuid.uuid4())} Defensive patterns
Strategy: validation
Validate before calling
claims = jwt.decode(assertion, options={"verify_signature": False})
jti = claims.get("jti")
if not isinstance(jti, str) or not jti:
raise ValueError("Assertion is missing a string jti claim; mint a new one") Type guard
def has_valid_jti(claims: dict) -> bool:
jti = claims.get("jti")
return isinstance(jti, str) and bool(jti) Prevention
- Always include a UUID-based string jti in assertion JWTs at mint time
- Never serialize jti as int, list, or object
- Decode and sanity-check assertion claims in integration tests before submitting
When it happens
Trigger: Calling validate() on an assertion JWT whose claims have no 'jti', an empty-string jti (falsy), or a non-string jti such as a list or dict (claims.get('jti') fails the `not jti or not isinstance(jti, str)` check at identity_assertion.py:452).
Common situations: Token minting code omits jti because it is optional in some JWT profiles; a custom issuer serializes jti as an integer or object; an IdP template leaves the jti field empty; hand-crafted test tokens forget the claim.
Related errors
- Assertion must include exp claim
- Assertion must include sub claim
- Assertion is not yet valid (nbf in future)
- Assertion iat is in the future
- Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/5539ff998cba533a.
Report an issue: GitHub.