PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion client_id {assertion_client_id!r} does not match a
Error message
Assertion client_id {assertion_client_id!r} does not match authenticated client {client_id!r} What it means
The assertion's signed `client_id` claim must match the `client_id` of the client presenting it in the token exchange. FastMCP binds assertions to the presenting client to prevent an assertion minted for one OAuth client from being replayed by another; a mismatch (or absent client_id) raises this error, deliberately before jti replay state is consumed.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:425
raise IdentityAssertionError("Assertion must include sub claim")
# 5. Required scopes on the issued access token derive from the assertion.
if self.config.required_scopes:
granted = set(_assertion_scopes(claims))
missing = set(self.config.required_scopes) - granted
if missing:
raise IdentityAssertionError(
f"Assertion missing required scopes: {sorted(missing)}"
)
# 6. The signed client_id and resource claims bind the assertion to the
# presenting client and this server. Checked here — before jti is
# recorded as consumed below — so an assertion presented with the
# wrong binding is rejected without burning replay protection for
# whichever client/server it actually belongs to.
assertion_client_id = claims.get("client_id")
if not assertion_client_id or assertion_client_id != client_id:
raise IdentityAssertionError(
f"Assertion client_id {assertion_client_id!r} does not match "
f"authenticated client {client_id!r}"
)
if resource_url is not None:
assertion_resource = claims.get("resource")
if not isinstance(assertion_resource, str) or not assertion_resource:
raise IdentityAssertionError("Assertion is missing resource claim")
if server_url_has_query(resource_url):
claim_matches = assertion_resource.rstrip("/") == resource_url.rstrip(
"/"
)
else:
claim_matches = normalize_resource_url(
assertion_resource
) == normalize_resource_url(resource_url)
if not claim_matches:
raise IdentityAssertionError(
f"Assertion resource {assertion_resource!r} does not match "View on GitHub (pinned to 1f02114297)
Solutions
- Ensure each client requests its own assertion from the IdP rather than reusing another client's.
- Configure the IdP to embed the presenting client's exact `client_id` in the assertion (matching the one used in the token exchange).
- Clear caches of pre-minted assertions when client credentials change.
- Log/compare both client_ids in the message to identify the binding mismatch.
Example fix
// before // client-b presents assertion minted with client_id="client-a" // after // client-b obtains a fresh assertion from the IdP authenticated as "client-b" assertion_claims["client_id"] == "client-b"
Defensive patterns
Strategy: validation
Validate before calling
def client_id_matches(claims: dict, presenting_client_id: str) -> bool:
cid = claims.get("client_id")
return isinstance(cid, str) and bool(cid) and cid == presenting_client_id Type guard
def bound_to_client(claims: dict, client_id: str) -> bool:
return claims.get("client_id") == client_id Try / catch
try:
token = await exchange(assertion)
except IdentityAssertionError as e:
if "does not match authenticated client" in str(e):
assertion = await obtain_own_assertion(client_id=MY_CLIENT_ID)
token = await exchange(assertion)
else:
raise Prevention
- Never share or cache assertions across different OAuth clients.
- Include client_id in the assertion minting request so the IdP binds it correctly.
- Invalidate cached assertions whenever client credentials rotate.
When it happens
Trigger: Calling `validate()` where `claims["client_id"] != client_id` — client B presents an assertion minted for client A; the assertion lacks client_id entirely; the IdP names the client differently (e.g. issuer uses a different client identifier format) than the one authenticating the exchange.
Common situations: Sharing a cached assertion file between two dev clients; proxy fronting the exchange with its own client credentials; IdP claim mapping placing the client identity under a different claim; rotating client credentials so the assertion was minted under the old client_id.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Assertion must include exp claim
- Assertion must include sub claim
- Assertion missing required scopes: {sorted(missing)}
- Assertion is missing resource claim
- Assertion must include a string jti claim
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/7b55593ceace0e4b.
Report an issue: GitHub.