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

  1. Add a unique, non-empty string 'jti' claim (e.g. a UUID) to every assertion JWT at mint time
  2. Verify the issuer does not serialize jti as a number/array/object — cast to str(uuid4()) before signing
  3. Decode the assertion locally (jwt.decode without verification) to inspect the actual claims before submitting
  4. 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

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


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