{"record":{"id":"9d29b674968b8156","repo":"PrefectHQ/fastmcp","slug":"assertion-is-not-yet-valid-nbf-in-future","errorCode":null,"errorMessage":"Assertion is not yet valid (nbf in future)","messagePattern":"Assertion is not yet valid \\(nbf in future\\)","errorType":"exception","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":391,"sourceCode":"            raise IdentityAssertionError(f\"Untrusted assertion issuer: {iss!r}\")\n\n        # 3. Verify signature, iss, aud, and exp via JWTVerifier.\n        verifier = await self._get_verifier(iss)\n        access_token = await verifier.load_access_token(assertion)\n        if access_token is None:\n            raise IdentityAssertionError(\n                \"Assertion failed signature/issuer/audience/expiry validation\"\n            )\n        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.","sourceCodeStart":373,"sourceCodeEnd":409,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L373-L409","documentation":"The assertion JWT's `nbf` (not-before) claim is further in the future than the server's current time plus the allowed clock-skew allowance (`CLOCK_SKEW_SECONDS`). The middleware rejects assertions that are not yet valid, per RFC 7523, so clients cannot pre-mint assertions for future use beyond the skew tolerance.","triggerScenarios":"Calling `validate()` with an assertion whose `nbf` exceeds `now + CLOCK_SKEW_SECONDS` — e.g. a client clock running fast, an issuer setting `nbf` to a future timestamp, or an issuer writing `nbf` in milliseconds instead of epoch seconds (values like 1.7e12 read as ~year 56000).","commonSituations":"Clock skew between client machine and server (VM resumed, NTP drift); issuer SDK misconfigured with a future validity window; a misencoded nbf (milliseconds vs seconds) from a custom minter.","solutions":["Fix the issuer to set `nbf` to the current time (or omit it entirely — nbf is optional).","Resynchronize the machine generating the assertion (NTP) if the clock is ahead of the server.","Verify nbf units are epoch seconds, not milliseconds; divide by 1000 if the issuer emits ms.","If modest skew is expected operationally, the server operator can raise `CLOCK_SKEW_SECONDS` on the identity assertion config."],"exampleFix":"// before\nclaims = {\"exp\": now + 300, \"nbf\": now + 3600}\n// after\nclaims = {\"exp\": now + 300, \"nbf\": now}","handlingStrategy":"validation","validationCode":"import time\n\ndef nbf_is_valid(claims: dict, skew: float = 60) -> bool:\n    nbf = claims.get(\"nbf\")\n    return nbf is None or (isinstance(nbf, (int, float)) and nbf <= time.time() + skew)","typeGuard":"def is_epoch_seconds(v) -> bool:\n    return isinstance(v, (int, float)) and not isinstance(v, bool) and v < 10_000_000_000","tryCatchPattern":"try:\n    token = await exchange(assertion)\nexcept IdentityAssertionError as e:\n    if \"not yet valid\" in str(e):\n        time.sleep(2)  # small skew, then retry once with a fresh assertion\n        assertion = mint_assertion()\n        token = await exchange(assertion)\n    else:\n        raise","preventionTips":["Omit nbf entirely — it is optional for assertions.","Sync clocks via NTP on machines minting assertions.","Emit epoch seconds, never milliseconds, for nbf."],"tags":["auth","jwt","clock-skew","identity-assertion"],"backgroundTag":"jwt-not-yet-valid","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}