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

  1. Re-mint the assertion so its payload is a JSON object containing iss, aud, exp, sub, etc.
  2. Base64url-decode the payload segment and confirm it is `{...}`.
  3. Fix custom token-generation code that json-dumps a non-dict claims value.
  4. 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

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


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