PrefectHQ/fastmcp · error · IdentityAssertionError

Assertion {name} claim must be a number

Error message

Assertion {name} claim must be a number

What it means

Raised by _numeric_date_claim() when a NumericDate-style claim (exp, nbf, iat) in the assertion claims is present but not a JSON number. JWT NumericDate claims must be int/float seconds since the epoch; booleans are explicitly rejected even though bool subclasses int in Python, because True/False are meaningless timestamps.

Source

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

def server_url_has_query(url: str) -> bool:
    """Check if a URL has query parameters."""
    return bool(urlparse(str(url)).query)


def _numeric_date_claim(claims: dict, name: str) -> float | None:
    """Read a NumericDate claim (RFC 7519 §2), rejecting non-numeric values.

    A validly-signed assertion could still carry a malformed `exp`/`iat`/`nbf`
    (e.g. a string, from a misbehaving IdP); comparing against it directly
    would raise `TypeError` outside the validation-error path. `bool` is
    excluded even though it subclasses `int` in Python — `true`/`false` are
    not timestamps.
    """
    value = claims.get(name)
    if value is None:
        return None
    if isinstance(value, bool) or not isinstance(value, (int, float)):
        raise IdentityAssertionError(f"Assertion {name} claim must be a number")
    return float(value)


def _assertion_scopes(claims: dict) -> list[str]:
    """Extract the scopes an ID-JAG grants, from `scope` or `scp`."""
    scope = claims.get("scope")
    if isinstance(scope, str):
        return scope.split()
    scp = claims.get("scp")
    if isinstance(scp, list):
        return [str(s) for s in scp]
    if isinstance(scp, str):
        return scp.split()
    return []


def _decode_unverified_claims(token: str) -> dict:
    """Decode a JWT payload without verifying the signature.

View on GitHub (pinned to 1f02114297)

Solutions

  1. Issue claims as epoch-seconds numbers: int(datetime.now(tz=UTC).timestamp()) + lifetime for exp
  2. Fix the issuer/serialization so datetime values become numeric timestamps, not ISO strings or str() output
  3. Decode the assertion locally and assert isinstance(claims['exp'], (int, float)) before calling validate
  4. In tests, build claims with numeric literals rather than strings

Example fix

// before
claims = {"exp": "2026-08-29T00:00:00Z"}
// after
claims = {"exp": int(datetime.now(tz=timezone.utc).timestamp()) + 300}
Defensive patterns

Strategy: type-guard

Validate before calling

claims = jwt.decode(assertion, options={"verify_signature": False})
for name in ("exp", "nbf", "iat"):
    v = claims.get(name)
    if v is not None and (isinstance(v, bool) or not isinstance(v, (int, float))):
        raise ValueError(f"{name} must be epoch-seconds number, got {type(v).__name__}")

Type guard

def is_numeric_date(v: object) -> bool:
    return not isinstance(v, bool) and isinstance(v, (int, float))

Prevention

When it happens

Trigger: Calling validate() on an assertion where 'exp', 'nbf', or 'iat' is a string like "1756000000", a nested object/array, a boolean true/false, or null-like non-numeric JSON — any value failing `isinstance(value, (int, float))` at identity_assertion.py:505.

Common situations: Token issuer serializes dates as ISO-8601 strings ("2026-08-29T00:00:00Z") instead of epoch seconds; template variables interpolate as strings; a bug passes datetime objects through a JSON encoder that emits strings; hand-built test claims use True/False placeholders.

Related errors


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