{"record":{"id":"9c9290c79a0f6f29","repo":"PrefectHQ/fastmcp","slug":"assertion-is-missing-resource-claim","errorCode":null,"errorMessage":"Assertion is missing resource claim","messagePattern":"Assertion is missing resource claim","errorType":"validation","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":432,"sourceCode":"                raise IdentityAssertionError(\n                    f\"Assertion missing required scopes: {sorted(missing)}\"\n                )\n\n        # 6. The signed client_id and resource claims bind the assertion to the\n        # presenting client and this server. Checked here — before jti is\n        # recorded as consumed below — so an assertion presented with the\n        # wrong binding is rejected without burning replay protection for\n        # whichever client/server it actually belongs to.\n        assertion_client_id = claims.get(\"client_id\")\n        if not assertion_client_id or assertion_client_id != client_id:\n            raise IdentityAssertionError(\n                f\"Assertion client_id {assertion_client_id!r} does not match \"\n                f\"authenticated client {client_id!r}\"\n            )\n        if resource_url is not None:\n            assertion_resource = claims.get(\"resource\")\n            if not isinstance(assertion_resource, str) or not assertion_resource:\n                raise IdentityAssertionError(\"Assertion is missing resource claim\")\n            if server_url_has_query(resource_url):\n                claim_matches = assertion_resource.rstrip(\"/\") == resource_url.rstrip(\n                    \"/\"\n                )\n            else:\n                claim_matches = normalize_resource_url(\n                    assertion_resource\n                ) == normalize_resource_url(resource_url)\n            if not claim_matches:\n                raise IdentityAssertionError(\n                    f\"Assertion resource {assertion_resource!r} does not match \"\n                    f\"this server {resource_url!r}\"\n                )\n\n        # 7. jti replay rejection (RFC 7523 §3). Must be a non-empty string —\n        # an array/object jti is unhashable and would raise TypeError on the\n        # cache lookup (a 500) instead of a clean invalid_grant.\n        jti = claims.get(\"jti\")","sourceCodeStart":414,"sourceCodeEnd":450,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L414-L450","documentation":"When the token exchange specifies a `resource` (resource indicator, RFC 8707), the assertion must carry a matching signed `resource` claim. FastMCP raises this error when the claim is absent, not a string, or empty — the assertion is not bound to this server, so it cannot be exchanged for a token targeting it.","triggerScenarios":"Calling `validate()` with resource_url set while the assertion payload has no `resource` key, `resource: null`, a non-string (e.g. a list of audiences), or an empty string.","commonSituations":"IdP not configured to include RFC 8707 resource indicators in assertions; an older issuer predating resource-indicator support; issuer emitting `aud`-style arrays under `resource`; client requesting a token exchange with a resource parameter while its IdP assertion template omits the claim.","solutions":["Configure the IdP/issuer to include the `resource` claim (the MCP server's URL) in assertions.","Ensure the claim is a plain non-empty string, not an array or null.","If the deployment doesn't use resource indicators, perform the exchange without a resource parameter so this check is skipped.","Upgrade the issuer if it predates RFC 8707 resource-indicator support."],"exampleFix":"// before\nclaims = {\"iss\": iss, \"sub\": sub, \"aud\": aud, \"exp\": now + 300}\n// after\nclaims = {\"iss\": iss, \"sub\": sub, \"aud\": aud, \"resource\": server_url, \"exp\": now + 300}","handlingStrategy":"validation","validationCode":"def has_resource_claim(claims: dict) -> bool:\n    r = claims.get(\"resource\")\n    return isinstance(r, str) and bool(r)","typeGuard":"def is_string_resource(claims: dict) -> bool:\n    return isinstance(claims.get(\"resource\"), str) and len(claims[\"resource\"]) > 0","tryCatchPattern":"try:\n    token = await exchange(assertion, resource=SERVER_URL)\nexcept IdentityAssertionError as e:\n    if \"missing resource claim\" in str(e):\n        raise ValueError(\"Issuer does not support RFC 8707 resource indicators\") from e\n    raise","preventionTips":["Configure the IdP to include the resource indicator in issued assertions.","Verify the claim is a string, not an audience-style array.","Only pass resource to the exchange if your issuer actually supports it."],"tags":["auth","oauth","resource-indicators","identity-assertion"],"backgroundTag":"missing-resource-claim","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}