PrefectHQ/fastmcp · error · IdentityAssertionError

Malformed assertion header: {e}

Error message

Malformed assertion header: {e}

What it means

The identity assertion's JOSE header could not be decoded: the token is not a decodable JWT-shaped string, or base64/JSON decoding of the header segment failed. FastMCP validates the header first (per SEP-990) before any network or key work, and converts decode failures into IdentityAssertionError (invalid_grant).

Source

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

                the jti is recorded as consumed, so an assertion presented by
                the wrong client is rejected without burning it for the right
                one.
            resource_url: This server's resource URL, if configured. Must match
                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:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Log/inspect the assertion on the client side before sending; ensure it is the raw id-jag JWT (three dot-separated base64url segments), not URL-encoded or quoted.
  2. Confirm the client is sending the correct token: the identity assertion from the id-jag exchange, not the access token itself.
  3. Fix client token handling (trim whitespace, don't base64 the token again, don't split across headers incorrectly).
  4. Verify the client library version produces the expected assertion format.

Example fix

// before: sending wrong token
assertion = access_token
// after: send the id-jag assertion itself
assertion = identity_assertion  # JWT with typ oauth-id-jag+jwt
Defensive patterns

Strategy: validation

Validate before calling

import base64, json
def looks_like_jwt(t: str) -> bool:
    parts = t.strip().split('.')
    if len(parts) != 3:
        return False
    try:
        json.loads(base64.urlsafe_b64decode(parts[0] + '=='))
        return True
    except Exception:
        return False

Type guard

def is_wellformed_jwt(token: str) -> bool:
    parts = token.split('.')
    return len(parts) == 3 and all(p for p in parts)

Try / catch

try:
    await provider.validate(assertion)
except IdentityAssertionError as e:
    if 'Malformed assertion header' in str(e):
        log.warning('client sent a non-JWT value as assertion')
    raise

Prevention

When it happens

Trigger: validate() is called with an assertion string whose first dot-delimited segment is not valid base64url or does not decode to a JSON object with the expected shape — decode_jwt_header raises ValueError/KeyError/IndexError.

Common situations: Client sends a truncated, whitespace-corrupted, URL-encoded, or double-wrapped JWT; the wrong token is passed (e.g. an opaque access token or refresh token instead of the id-jag assertion); test fixtures with hand-mangled tokens.

Understand the failure class

Related errors


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