{"record":{"id":"58a20f789b9c1208","repo":"PrefectHQ/fastmcp","slug":"assertion-exp-too-far-in-future-max-self-max-ass","errorCode":null,"errorMessage":"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)","messagePattern":"Assertion exp too far in future \\(max (.+?)s\\)","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":612,"sourceCode":"        if not exp:\n            raise ValueError(\"Assertion must include exp claim\")\n\n        # Validate exp is in the future (with small clock skew tolerance)\n        if exp < now - 30:  # 30 second clock skew tolerance\n            raise ValueError(\"Assertion has expired\")\n\n        # If iat is present, validate it and check assertion lifetime\n        if iat:\n            if iat > now + 30:  # 30 second clock skew tolerance\n                raise ValueError(\"Assertion iat is in the future\")\n            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)","sourceCodeStart":594,"sourceCodeEnd":630,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L594-L630","documentation":"Raised when no 'iat' claim is present and the assertion's 'exp' lies further than MAX_ASSERTION_LIFETIME beyond the current server time. This is the fallback lifetime cap for assertions lacking iat — the server bounds validity from 'now' instead of from issuance.","triggerScenarios":"An assertion with no iat but a distant exp (e.g. exp = now + 86400); signing tokens with only exp because an older OAuth template omitted iat.","commonSituations":"Templates from token libraries where iat is optional; teams deliberately omitting iat yet keeping long exp windows from previous setups.","solutions":["Shorten exp so it is within MAX_ASSERTION_LIFETIME of now","Add a correct 'iat' claim (now) so lifetime is measured from issuance and you can use the full window legitimately","Regenerate assertions per request with short exps"],"exampleFix":"// before\npayload = {\"exp\": now + 86400}  # no iat, huge exp\n// after\npayload = {\"iat\": now, \"exp\": now + 300}","handlingStrategy":"validation","validationCode":"import time\nclaims = jwt.decode(token, options={\"verify_signature\": False})\nif \"iat\" not in claims and claims[\"exp\"] > time.time() + 300:\n    raise ValueError(\"assertion without iat must keep exp within server max from now\")","typeGuard":"def no_iat_exp_bounded(claims: dict, max_lifetime: int) -> bool:\n    if \"iat\" in claims:\n        return True\n    return isinstance(claims.get(\"exp\"), (int, float)) and \\\n        claims[\"exp\"] <= time.time() + max_lifetime","tryCatchPattern":"try:\n    validator.validate_assertion(token, client_id, jwks)\nexcept ValueError as e:\n    if \"exp too far in future\" in str(e):\n        token = mint_assertion(client_id, lifetime=300, include_iat=True)\n    else:\n        raise","preventionTips":["Always include iat so lifetime is measured from issuance","Keep exp within the server's max window either way","Reuse a shared, tested assertion-minting function"],"tags":["oauth","jwt","validation","lifetime"],"backgroundTag":"jwt-lifetime-too-long","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}