{"record":{"id":"21b762a51f081195","repo":"PrefectHQ/fastmcp","slug":"assertion-has-expired","errorCode":null,"errorMessage":"Assertion has expired","messagePattern":"Assertion has expired","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":599,"sourceCode":"\n        # 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)\n        access_token = await verifier.load_access_token(assertion)\n        if not access_token:\n            raise ValueError(\"Invalid JWT assertion\")\n\n        claims = access_token.claims\n\n        # 3. Validate assertion lifetime (exp and iat)\n        now = time.time()\n        exp = claims.get(\"exp\")\n        iat = claims.get(\"iat\")\n\n        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:","sourceCodeStart":581,"sourceCodeEnd":617,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L581-L617","documentation":"Raised by validate_assertion when the assertion's 'exp' claim is older than the current time minus a 30-second clock-skew allowance, i.e. the JWT has expired. The library enforces assertion freshness per RFC 7523 so replayed stale assertions are rejected.","triggerScenarios":"Presenting a cached or long-lived client assertion after its exp timestamp passed; a machine clock set more than 30 seconds behind the server's; reusing a persisted token file from a previous session.","commonSituations":"Clients caching the signed assertion instead of minting a fresh one per token request; VMs/containers with clock drift; slow requests where a short (e.g. 60s) exp lapsed before validation.","solutions":["Mint a fresh assertion at request time (exp = now + 300) instead of caching it","Synchronize the machine clock (NTP) if drift is the cause","Increase the assertion's exp window slightly if your flow legitimately needs longer validity (respecting the server's MAX_ASSERTION_LIFETIME)"],"exampleFix":"// before\nexp = iat + 3600  # long-lived assertion gets cached and reused\n// after\nexp = int(time.time()) + 300  # mint fresh per request","handlingStrategy":"validation","validationCode":"import time\nclaims = jwt.decode(token, options={\"verify_signature\": False})\nif claims.get(\"exp\", 0) <= time.time():\n    token = mint_fresh_assertion(client_id)  # re-mint before calling","typeGuard":"def is_currently_valid(claims: dict, skew: float = 30) -> bool:\n    return isinstance(claims.get(\"exp\"), (int, float)) and claims[\"exp\"] > time.time() - skew","tryCatchPattern":"try:\n    validator.validate_assertion(token, client_id, jwks)\nexcept ValueError as e:\n    if \"expired\" in str(e):\n        token = mint_fresh_assertion(client_id)\n        validator.validate_assertion(token, client_id, jwks)\n    else:\n        raise","preventionTips":["Mint assertions per-request instead of caching them","Run NTP on all hosts minting/validating tokens","Keep assertion lifetimes short (~5 minutes)"],"tags":["oauth","jwt","token-expired","clock-skew"],"backgroundTag":"jwt-token-expired","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}