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
- Issue claims as epoch-seconds numbers: int(datetime.now(tz=UTC).timestamp()) + lifetime for exp
- Fix the issuer/serialization so datetime values become numeric timestamps, not ISO strings or str() output
- Decode the assertion locally and assert isinstance(claims['exp'], (int, float)) before calling validate
- 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
- Issue NumericDate claims as epoch seconds (int), never ISO strings
- Test round-trip: sign, decode, and assert claim types before deploying an issuer
- Beware Python bool passing isinstance(int) — treat booleans as invalid
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
- CIMD document must have jwks_uri or jwks for private_key_jwt
- Assertion must include exp claim
- Assertion iat is in the future
- Assertion lifetime too long: {exp - iat}s (max {self.MAX_ASS
- Assertion exp too far in future (max {self.MAX_ASSERTION_LIF
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/786b6f5cd303eb7c.
Report an issue: GitHub.