{"record":{"id":"98105bf97dd76266","repo":"PrefectHQ/fastmcp","slug":"malformed-assertion-header-e","errorCode":null,"errorMessage":"Malformed assertion header: {e}","messagePattern":"Malformed assertion header: (.+?)","errorType":"exception","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":354,"sourceCode":"                the jti is recorded as consumed, so an assertion presented by\n                the wrong client is rejected without burning it for the right\n                one.\n            resource_url: This server's resource URL, if configured. Must match\n                the assertion's signed `resource` claim, for the same reason.\n\n        Returns:\n            The verified claims (including `sub`, `iss`, and any `resource`/`scope`).\n\n        Raises:\n            IdentityAssertionError: If the assertion is invalid for any reason.\n        \"\"\"\n        self._maybe_cleanup()\n\n        # 1. typ header MUST be oauth-id-jag+jwt (SEP-990 §5.1).\n        try:\n            header = decode_jwt_header(assertion)\n        except (ValueError, KeyError, IndexError) as e:\n            raise IdentityAssertionError(f\"Malformed assertion header: {e}\") from e\n        if not isinstance(header, dict):\n            # A JSON-array/scalar header is valid JSON but not a JOSE header;\n            # guard before .get() so this maps to invalid_grant, not a 500.\n            raise IdentityAssertionError(\"Assertion JOSE header must be a JSON object\")\n        if header.get(\"typ\") != ID_JAG_TYP:\n            raise IdentityAssertionError(\n                f\"Assertion typ must be {ID_JAG_TYP!r}, got {header.get('typ')!r}\"\n            )\n\n        # 2. iss must be a trusted issuer before we fetch any keys for it.\n        try:\n            unverified_claims = _decode_unverified_claims(assertion)\n        except (ValueError, KeyError, IndexError) as e:\n            raise IdentityAssertionError(f\"Malformed assertion payload: {e}\") from e\n        if not isinstance(unverified_claims, dict):\n            raise IdentityAssertionError(\"Assertion payload is not a JSON object\")\n        iss = unverified_claims.get(\"iss\")\n        if not iss or iss not in self.config.trusted_issuers:","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L336-L372","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: sending wrong token\nassertion = access_token\n// after: send the id-jag assertion itself\nassertion = identity_assertion  # JWT with typ oauth-id-jag+jwt","handlingStrategy":"validation","validationCode":"import base64, json\ndef looks_like_jwt(t: str) -> bool:\n    parts = t.strip().split('.')\n    if len(parts) != 3:\n        return False\n    try:\n        json.loads(base64.urlsafe_b64decode(parts[0] + '=='))\n        return True\n    except Exception:\n        return False","typeGuard":"def is_wellformed_jwt(token: str) -> bool:\n    parts = token.split('.')\n    return len(parts) == 3 and all(p for p in parts)","tryCatchPattern":"try:\n    await provider.validate(assertion)\nexcept IdentityAssertionError as e:\n    if 'Malformed assertion header' in str(e):\n        log.warning('client sent a non-JWT value as assertion')\n    raise","preventionTips":["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"],"tags":["jwt","malformed","parsing"],"backgroundTag":"malformed-jwt","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}