PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion payload is not a JSON object
Error message
Assertion payload is not a JSON object
What it means
The assertion payload decoded as JSON but is not an object (array, string, number, etc.), so claim lookup via .get() would fail. FastMCP guards this to return invalid_grant rather than a 500.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:370
header = decode_jwt_header(assertion)
except (ValueError, KeyError, IndexError) as e:
raise IdentityAssertionError(f"Malformed assertion header: {e}") from e
if not isinstance(header, dict):
# A JSON-array/scalar header is valid JSON but not a JOSE header;
# guard before .get() so this maps to invalid_grant, not a 500.
raise IdentityAssertionError("Assertion JOSE header must be a JSON object")
if header.get("typ") != ID_JAG_TYP:
raise IdentityAssertionError(
f"Assertion typ must be {ID_JAG_TYP!r}, got {header.get('typ')!r}"
)
# 2. iss must be a trusted issuer before we fetch any keys for it.
try:
unverified_claims = _decode_unverified_claims(assertion)
except (ValueError, KeyError, IndexError) as e:
raise IdentityAssertionError(f"Malformed assertion payload: {e}") from e
if not isinstance(unverified_claims, dict):
raise IdentityAssertionError("Assertion payload is not a JSON object")
iss = unverified_claims.get("iss")
if not iss or iss not in self.config.trusted_issuers:
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:View on GitHub (pinned to 1f02114297)
Solutions
- Re-mint the assertion so its payload is a JSON object containing iss, aud, exp, sub, etc.
- Base64url-decode the payload segment and confirm it is `{...}`.
- Fix custom token-generation code that json-dumps a non-dict claims value.
- If a third party issues such tokens, reject upstream and report to that provider.
Example fix
// before (invalid claims payload)
["sub","alice"]
// after
{"iss": "https://idp.example.com", "sub": "alice", "exp": 1735689600} Defensive patterns
Strategy: type-guard
Validate before calling
import base64, json
claims = json.loads(base64.urlsafe_b64decode(assertion.split('.')[1] + '=='))
assert isinstance(claims, dict), 'claims must be a JSON object' Type guard
def claims_are_object(token: str) -> bool:
import base64, json
c = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '=='))
return isinstance(c, dict) Try / catch
try:
await provider.validate(assertion)
except IdentityAssertionError as e:
if 'payload is not a JSON object' in str(e):
log.warning('malformed claims; reject token at source')
raise Prevention
- Mint claims as a dict with standard JWT libraries
- Validate claims shape before sending
- Reject non-object payloads in client-side checks
- Audit custom token-minting code for json.dumps of non-dicts
When it happens
Trigger: validate() receives an assertion whose claims segment decodes to a non-dict JSON value — `if not isinstance(unverified_claims, dict)` fires.
Common situations: Custom/broken token minters that serialize claims as a list or string; fuzzed or maliciously crafted tokens; corrupted stored tokens.
Related errors
- Assertion JOSE header must be a JSON object
- OIDC discovery document for issuer {issuer!r} is not a JSON
- Malformed assertion header: {e}
- Malformed assertion payload: {e}
- Invalid JWKS JSON: {e}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/4b38e890e0a0817e.
Report an issue: GitHub.