{"record":{"id":"4297a9220af8a93a","repo":"PrefectHQ/fastmcp","slug":"assertion-exp-too-far-in-future-max-self-max-ass-4297a9","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":"validation","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":400,"sourceCode":"        claims = access_token.claims\n\n        now = time.time()\n        exp = _numeric_date_claim(claims, \"exp\")\n        iat = _numeric_date_claim(claims, \"iat\")\n        nbf = _numeric_date_claim(claims, \"nbf\")\n        if exp is None:\n            raise IdentityAssertionError(\"Assertion must include exp claim\")\n        if nbf is not None and nbf > now + self.CLOCK_SKEW_SECONDS:\n            raise IdentityAssertionError(\"Assertion is not yet valid (nbf in future)\")\n        if iat is not None:\n            if iat > now + self.CLOCK_SKEW_SECONDS:\n                raise IdentityAssertionError(\"Assertion iat is in the future\")\n            if exp - iat > self.MAX_ASSERTION_LIFETIME:\n                raise IdentityAssertionError(\n                    f\"Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s)\"\n                )\n        elif exp > now + self.MAX_ASSERTION_LIFETIME:\n            raise IdentityAssertionError(\n                f\"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)\"\n            )\n\n        # 4. sub is mandatory (RFC 7523 §3) — it identifies the end user.\n        sub = claims.get(\"sub\")\n        if not sub:\n            raise IdentityAssertionError(\"Assertion must include sub claim\")\n\n        # 5. Required scopes on the issued access token derive from the assertion.\n        if self.config.required_scopes:\n            granted = set(_assertion_scopes(claims))\n            missing = set(self.config.required_scopes) - granted\n            if missing:\n                raise IdentityAssertionError(\n                    f\"Assertion missing required scopes: {sorted(missing)}\"\n                )\n\n        # 6. The signed client_id and resource claims bind the assertion to the","sourceCodeStart":382,"sourceCodeEnd":418,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L382-L418","documentation":"When the assertion has no `iat` claim, the middleware falls back to checking `exp` directly against wall-clock time: an expiry more than `MAX_ASSERTION_LIFETIME` seconds in the future is rejected. This prevents clients from skipping the iat-based lifetime cap simply by omitting iat.","triggerScenarios":"Calling `validate()` with an assertion that omits `iat` but sets `exp` more than MAX_ASSERTION_LIFETIME seconds ahead of the server's now — e.g. exp set hours/days out on an iat-less token.","commonSituations":"Issuers that emit exp-only JWTs (common for opaque access tokens) reused as identity assertions; hand-built test JWTs with only iss/sub/aud/exp; template tokens with long default expiries.","solutions":["Include `iat` in the assertion and set `exp = iat + short_lifetime`.","Shorten the exp so `exp <= now + MAX_ASSERTION_LIFETIME` (e.g. now + 300).","If longer windows are required, the server operator can raise `MAX_ASSERTION_LIFETIME` in the config.","Verify with a JWT decoder that exp is a near-future epoch-seconds value."],"exampleFix":"// before\nclaims = {\"iss\": iss, \"sub\": sub, \"aud\": aud, \"exp\": now + 7200}\n// after\nclaims = {\"iss\": iss, \"sub\": sub, \"aud\": aud, \"iat\": now, \"exp\": now + 300}","handlingStrategy":"validation","validationCode":"import time\n\ndef exp_not_too_far(claims: dict, max_lifetime: int = 300) -> bool:\n    exp = claims.get(\"exp\")\n    return isinstance(exp, (int, float)) and exp <= time.time() + max_lifetime","typeGuard":"def has_near_exp(claims, max_lifetime=300) -> bool:\n    exp = claims.get(\"exp\")\n    return isinstance(exp, (int, float)) and not isinstance(exp, bool)","tryCatchPattern":"try:\n    token = await exchange(assertion)\nexcept IdentityAssertionError as e:\n    if \"exp too far in future\" in str(e):\n        assertion = mint_assertion(iat=int(time.time()), exp=int(time.time()) + 300)\n        token = await exchange(assertion)\n    else:\n        raise","preventionTips":["Always include iat so the lifetime is measured from issuance, not wall clock.","Cap exp at now + a few minutes when minting assertions.","Check issuer templates for long default expiries and override them."],"tags":["auth","jwt","token-lifetime","identity-assertion"],"backgroundTag":"jwt-exp-too-far-in-future","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}