PrefectHQ/fastmcp · error · IdentityAssertionError

Assertion JOSE header must be a JSON object

Error message

Assertion JOSE header must be a JSON object

What it means

The assertion's header decoded to valid JSON but is not a JSON object — a JOSE header must be an object for .get() checks to work. FastMCP guards this so a pathological token maps to invalid_grant instead of crashing with a 500.

Source

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

                the assertion's signed `resource` claim, for the same reason.

        Returns:
            The verified claims (including `sub`, `iss`, and any `resource`/`scope`).

        Raises:
            IdentityAssertionError: If the assertion is invalid for any reason.
        """
        self._maybe_cleanup()

        # 1. typ header MUST be oauth-id-jag+jwt (SEP-990 §5.1).
        try:
            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)

View on GitHub (pinned to 1f02114297)

Solutions

  1. Regenerate the assertion with a standard JWT library (the header must be a JSON object with alg/typ).
  2. Inspect the header: base64url-decode the first segment and confirm it is `{...}`.
  3. Fix any custom token-minting code that dumps a list or string as the header.
  4. If tokens come from a third party, report the malformed token issue to that provider.

Example fix

// before (invalid header payload)
["alg","HS256"]
// after
{"alg": "RS256", "typ": "oauth-id-jag+jwt"}
Defensive patterns

Strategy: type-guard

Validate before calling

import base64, json
header = json.loads(base64.urlsafe_b64decode(assertion.split('.')[0] + '=='))
assert isinstance(header, dict), 'JOSE header must be a JSON object'

Type guard

def header_is_object(token: str) -> bool:
    import base64, json
    h = json.loads(base64.urlsafe_b64decode(token.split('.')[0] + '=='))
    return isinstance(h, dict)

Try / catch

try:
    await provider.validate(assertion)
except IdentityAssertionError as e:
    if 'JOSE header must be a JSON object' in str(e):
        log.warning('assertion header is not a JSON object; re-mint token')
    raise

Prevention

When it happens

Trigger: validate() receives an assertion whose header segment base64-decodes to a JSON array or scalar (e.g. `[1,2]`, `"abc"`, `null`).

Common situations: Hand-crafted or fuzzer-generated tokens in tests; a broken token-minting implementation serializing the header incorrectly; corrupted tokens stored/tranformed by middleware.

Related errors


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