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
- 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.
- Confirm the client is sending the correct token: the identity assertion from the id-jag exchange, not the access token itself.
- Fix client token handling (trim whitespace, don't base64 the token again, don't split across headers incorrectly).
- 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
- Send the raw JWT string; never URL-encode or base64-wrap it
- Validate token shape client-side before sending
- Beware proxies/header size limits truncating tokens
- Log token length (not content) to spot truncation
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Malformed assertion payload: {e}
- Failed to extract key ID from token: {e}
- Assertion JOSE header must be a JSON object
- Assertion payload is not a JSON object
- Invalid client_assertion_type: expected {JWT_BEARER_ASSERTIO
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/98105bf97dd76266.
Report an issue: GitHub.