PrefectHQ/fastmcp · error · IdentityAssertionError

Assertion is not yet valid (nbf in future)

Error message

Assertion is not yet valid (nbf in future)

What it means

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.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:391

            raise IdentityAssertionError(f"Untrusted assertion issuer: {iss!r}")

        # 3. Verify signature, iss, aud, and exp via JWTVerifier.
        verifier = await self._get_verifier(iss)
        access_token = await verifier.load_access_token(assertion)
        if access_token is None:
            raise IdentityAssertionError(
                "Assertion failed signature/issuer/audience/expiry validation"
            )
        claims = access_token.claims

        now = time.time()
        exp = _numeric_date_claim(claims, "exp")
        iat = _numeric_date_claim(claims, "iat")
        nbf = _numeric_date_claim(claims, "nbf")
        if exp is None:
            raise IdentityAssertionError("Assertion must include exp claim")
        if nbf is not None and nbf > now + self.CLOCK_SKEW_SECONDS:
            raise IdentityAssertionError("Assertion is not yet valid (nbf in future)")
        if iat is not None:
            if iat > now + self.CLOCK_SKEW_SECONDS:
                raise IdentityAssertionError("Assertion iat is in the future")
            if exp - iat > self.MAX_ASSERTION_LIFETIME:
                raise IdentityAssertionError(
                    f"Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s)"
                )
        elif exp > now + self.MAX_ASSERTION_LIFETIME:
            raise IdentityAssertionError(
                f"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)"
            )

        # 4. sub is mandatory (RFC 7523 §3) — it identifies the end user.
        sub = claims.get("sub")
        if not sub:
            raise IdentityAssertionError("Assertion must include sub claim")

        # 5. Required scopes on the issued access token derive from the assertion.

View on GitHub (pinned to 1f02114297)

Solutions

  1. Fix the issuer to set `nbf` to the current time (or omit it entirely — nbf is optional).
  2. Resynchronize the machine generating the assertion (NTP) if the clock is ahead of the server.
  3. Verify nbf units are epoch seconds, not milliseconds; divide by 1000 if the issuer emits ms.
  4. If modest skew is expected operationally, the server operator can raise `CLOCK_SKEW_SECONDS` on the identity assertion config.

Example fix

// before
claims = {"exp": now + 300, "nbf": now + 3600}
// after
claims = {"exp": now + 300, "nbf": now}
Defensive patterns

Strategy: validation

Validate before calling

import time

def nbf_is_valid(claims: dict, skew: float = 60) -> bool:
    nbf = claims.get("nbf")
    return nbf is None or (isinstance(nbf, (int, float)) and nbf <= time.time() + skew)

Type guard

def is_epoch_seconds(v) -> bool:
    return isinstance(v, (int, float)) and not isinstance(v, bool) and v < 10_000_000_000

Try / catch

try:
    token = await exchange(assertion)
except IdentityAssertionError as e:
    if "not yet valid" in str(e):
        time.sleep(2)  # small skew, then retry once with a fresh assertion
        assertion = mint_assertion()
        token = await exchange(assertion)
    else:
        raise

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/9d29b674968b8156. Report an issue: GitHub.