{"record":{"id":"786b6f5cd303eb7c","repo":"PrefectHQ/fastmcp","slug":"assertion-name-claim-must-be-a-number","errorCode":null,"errorMessage":"Assertion {name} claim must be a number","messagePattern":"Assertion (.+?) claim must be a number","errorType":"validation","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":505,"sourceCode":"def server_url_has_query(url: str) -> bool:\n    \"\"\"Check if a URL has query parameters.\"\"\"\n    return bool(urlparse(str(url)).query)\n\n\ndef _numeric_date_claim(claims: dict, name: str) -> float | None:\n    \"\"\"Read a NumericDate claim (RFC 7519 §2), rejecting non-numeric values.\n\n    A validly-signed assertion could still carry a malformed `exp`/`iat`/`nbf`\n    (e.g. a string, from a misbehaving IdP); comparing against it directly\n    would raise `TypeError` outside the validation-error path. `bool` is\n    excluded even though it subclasses `int` in Python — `true`/`false` are\n    not timestamps.\n    \"\"\"\n    value = claims.get(name)\n    if value is None:\n        return None\n    if isinstance(value, bool) or not isinstance(value, (int, float)):\n        raise IdentityAssertionError(f\"Assertion {name} claim must be a number\")\n    return float(value)\n\n\ndef _assertion_scopes(claims: dict) -> list[str]:\n    \"\"\"Extract the scopes an ID-JAG grants, from `scope` or `scp`.\"\"\"\n    scope = claims.get(\"scope\")\n    if isinstance(scope, str):\n        return scope.split()\n    scp = claims.get(\"scp\")\n    if isinstance(scp, list):\n        return [str(s) for s in scp]\n    if isinstance(scp, str):\n        return scp.split()\n    return []\n\n\ndef _decode_unverified_claims(token: str) -> dict:\n    \"\"\"Decode a JWT payload without verifying the signature.","sourceCodeStart":487,"sourceCodeEnd":523,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L487-L523","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nclaims = {\"exp\": \"2026-08-29T00:00:00Z\"}\n// after\nclaims = {\"exp\": int(datetime.now(tz=timezone.utc).timestamp()) + 300}","handlingStrategy":"type-guard","validationCode":"claims = jwt.decode(assertion, options={\"verify_signature\": False})\nfor name in (\"exp\", \"nbf\", \"iat\"):\n    v = claims.get(name)\n    if v is not None and (isinstance(v, bool) or not isinstance(v, (int, float))):\n        raise ValueError(f\"{name} must be epoch-seconds number, got {type(v).__name__}\")","typeGuard":"def is_numeric_date(v: object) -> bool:\n    return not isinstance(v, bool) and isinstance(v, (int, float))","tryCatchPattern":null,"preventionTips":["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"],"tags":["jwt","validation","timestamp","auth"],"backgroundTag":"invalid-jwt-claim-type","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}