{"record":{"id":"fe2587ae7bed8eac","repo":"PrefectHQ/fastmcp","slug":"assertion-must-include-jti-claim","errorCode":null,"errorMessage":"Assertion must include jti claim","messagePattern":"Assertion must include jti claim","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":623,"sourceCode":"            if exp - iat > self.MAX_ASSERTION_LIFETIME:\n                raise ValueError(\n                    f\"Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASSERTION_LIFETIME}s)\"\n                )\n        else:\n            # No iat, enforce max lifetime from now\n            if exp > now + self.MAX_ASSERTION_LIFETIME:\n                raise ValueError(\n                    f\"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)\"\n                )\n\n        # 4. Additional RFC 7523 validation: sub claim must equal client_id\n        if claims.get(\"sub\") != client_id:\n            raise ValueError(f\"Assertion sub claim must be {client_id}\")\n\n        # 5. Check jti for replay attacks (RFC 7523 requirement)\n        jti = claims.get(\"jti\")\n        if not jti:\n            raise ValueError(\"Assertion must include jti claim\")\n\n        # Check if JTI was already used (and hasn't expired from cache)\n        if jti in self._jti_cache:\n            cached_exp = self._jti_cache[jti]\n            if cached_exp > now:  # Still valid in cache\n                raise ValueError(f\"Assertion replay detected: jti {jti} already used\")\n            # Expired in cache, can be reused (clean it up)\n            del self._jti_cache[jti]\n\n        # Emergency size limit (shouldn't hit with proper TTL cleanup)\n        if len(self._jti_cache) >= self._jti_cache_max_size:\n            self._cleanup_expired_jtis()\n            # If still over limit after cleanup, reject to prevent DoS\n            if len(self._jti_cache) >= self._jti_cache_max_size:\n                self.logger.warning(\n                    \"JTI cache at max capacity (%d), possible attack\",\n                    self._jti_cache_max_size,\n                )","sourceCodeStart":605,"sourceCodeEnd":641,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L605-L641","documentation":"Raised by validate_assertion when the client assertion lacks the 'jti' (JWT ID) claim. RFC 7523 recommends jti for replay prevention; this implementation requires it so every assertion has a unique identifier it can track in its replay cache.","triggerScenarios":"Minting an assertion payload without jti; using a JWT helper whose default claim set omits jti; stripping optional claims during token serialization.","commonSituations":"Hand-rolled assertion builders; minimal test fixtures that omit jti; older minting code predating replay-protection requirements.","solutions":["Add a unique 'jti' value (e.g. str(uuid.uuid4())) to every assertion payload","Generate a fresh jti per assertion, never reuse it while the old assertion is unexpired","Update assertion templates/SDK settings to include jti by default"],"exampleFix":"// before\npayload = {\"iss\": client_id, \"sub\": client_id, \"aud\": aud, \"iat\": now, \"exp\": now + 300}\n// after\npayload = {\"iss\": client_id, \"sub\": client_id, \"aud\": aud, \"iat\": now, \"exp\": now + 300,\n           \"jti\": str(uuid.uuid4())}","handlingStrategy":"validation","validationCode":"import uuid\nclaims = jwt.decode(token, options={\"verify_signature\": False})\nif not claims.get(\"jti\"):\n    token = mint_assertion(client_id, jti=str(uuid.uuid4()))","typeGuard":"def has_jti(claims: dict) -> bool:\n    return bool(claims.get(\"jti\"))","tryCatchPattern":"try:\n    validator.validate_assertion(token, client_id, jwks)\nexcept ValueError as e:\n    if \"must include jti\" in str(e):\n        token = mint_assertion(client_id, jti=str(uuid.uuid4()))\n    else:\n        raise","preventionTips":["Always generate a fresh uuid4 jti per assertion","Keep jti in your claim template/checklist","Test minted assertions for exp/iat/jti presence"],"tags":["oauth","jwt","rfc7523","replay-protection"],"backgroundTag":"missing-jwt-claim","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}