{"record":{"id":"f54231e4103dcefe","repo":"PrefectHQ/fastmcp","slug":"assertion-must-include-exp-claim","errorCode":null,"errorMessage":"Assertion must include exp claim","messagePattern":"Assertion must include exp claim","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/cimd.py","lineNumber":595,"sourceCode":"        else:\n            raise ValueError(\n                \"CIMD document must have jwks_uri or jwks for private_key_jwt\"\n            )\n\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)\"","sourceCodeStart":577,"sourceCodeEnd":613,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/cimd.py#L577-L613","documentation":"This ValueError is raised by CIMDValidator.validate_assertion when the private_key_jwt client assertion (a JWT) lacks the mandatory 'exp' (expiration) claim. RFC 7523 requires exp so the server can bound the assertion's lifetime; without it the assertion could never expire, so the library refuses it outright before any other lifetime checks.","triggerScenarios":"Calling validate_assertion (directly or via validate_private_key_jwt) with a signed JWT whose payload has no 'exp' claim — e.g. a hand-built assertion payload dict that only includes iss/sub/aud/iat/jti, or a token minted by a custom JWT helper that omits exp.","commonSituations":"Developers manually constructing client assertions for OAuth private_key_jwt auth and forgetting exp; migrating from a token format that didn't require exp; using a JWT library where exp is optional by default.","solutions":["Add an 'exp' claim (Unix timestamp, seconds) to the assertion payload, typically now + 300 for a 5-minute lifetime","Ensure exp is a numeric (int) timestamp, not an ISO string, since the code compares exp < now - 30","Regenerate the assertion with a standard OIDC-compliant JWT helper that always sets exp"],"exampleFix":"// before\npayload = {\"iss\": client_id, \"sub\": client_id, \"aud\": token_endpoint, \"iat\": now, \"jti\": jti}\n// after\npayload = {\"iss\": client_id, \"sub\": client_id, \"aud\": token_endpoint, \"iat\": now,\n           \"exp\": now + 300, \"jti\": jti}","handlingStrategy":"validation","validationCode":"import time\nclaims = jwt.decode(assertion, options={\"verify_signature\": False})\nif not claims.get(\"exp\"):\n    raise ValueError(\"assertion payload must include a numeric exp claim\")\nif not isinstance(claims[\"exp\"], (int, float)):\n    raise TypeError(\"exp must be a Unix timestamp\")","typeGuard":"def has_exp(claims: dict) -> bool:\n    exp = claims.get(\"exp\")\n    return isinstance(exp, (int, float)) and exp > 0","tryCatchPattern":null,"preventionTips":["Always include exp when building JWT payloads","Use a standard OIDC assertion helper that sets exp by default","Write a unit test asserting presence of exp, iat, jti in minted assertions"],"tags":["oauth","jwt","private-key-jwt","validation"],"backgroundTag":"missing-jwt-claim","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}