PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion resource {assertion_resource!r} does not match thi
Error message
Assertion resource {assertion_resource!r} does not match this server {resource_url!r} What it means
The assertion carries a `resource` claim but it does not match the resource URL of this MCP server (compared after normalization, or via rstrip('/') when the requested resource URL has a query string). FastMCP requires the signed resource binding to match the server receiving the exchange, preventing an assertion minted for one server from being redeemed at another.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:442
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 "
f"this server {resource_url!r}"
)
# 7. jti replay rejection (RFC 7523 §3). Must be a non-empty string —
# an array/object jti is unhashable and would raise TypeError on the
# cache lookup (a 500) instead of a clean invalid_grant.
jti = claims.get("jti")
if not jti or not isinstance(jti, str):
raise IdentityAssertionError("Assertion must include a string jti claim")
cached_exp = self._jti_cache.get(jti)
if cached_exp is not None and cached_exp > now:
raise IdentityAssertionError(f"Assertion replay detected: jti {jti} reused")
# Enforce the cap BEFORE inserting so a rejected assertion never grows the
# cache. A fresh jti that would exceed capacity is rejected outright (after
# a cleanup pass to reclaim any expired entries first).
if (View on GitHub (pinned to 1f02114297)
Solutions
- Update the issuer's resource-indicator config to emit this server's exact public URL.
- Restart/re-authenticate so newly minted assertions carry the current server URL.
- Compare the two URLs in the error message and reconcile scheme, host, port, and path differences.
- Ensure the server's configured base URL (what clients request as resource) matches what the IdP signs.
Example fix
// before
claims = {"resource": "https://old.example.com/mcp", ...} # presented at https://new.example.com/mcp
// after
claims = {"resource": "https://new.example.com/mcp", ...} Defensive patterns
Strategy: validation
Validate before calling
def resource_matches(claims: dict, server_url: str) -> bool:
r = claims.get("resource")
return isinstance(r, str) and r.rstrip("/") == server_url.rstrip("/") Type guard
def resource_bound_here(claims: dict, server_url: str) -> bool:
r = claims.get("resource")
return isinstance(r, str) and r.rstrip("/") != "" and r.rstrip("/") == server_url.rstrip("/") Try / catch
try:
token = await exchange(assertion, resource=SERVER_URL)
except IdentityAssertionError as e:
if "does not match this server" in str(e):
assertion = await obtain_assertion(resource=SERVER_URL) # re-mint for this server
token = await exchange(assertion, resource=SERVER_URL)
else:
raise Prevention
- Keep the IdP's resource-indicator allowlist updated with the server's current public URL.
- Use one canonical server base URL everywhere (config, docs, IdP registration).
- Request a fresh assertion when the server's URL changes rather than reusing cached ones.
When it happens
Trigger: Calling `validate()` with resource_url set and a string `resource` claim that normalizes to a different URL — assertion minted for https://server-a presented at https://server-b; trailing-slash/scheme/port differences that normalization doesn't collapse; localhost vs 127.0.0.1; env-specific base URLs (staging vs prod) baked into the assertion.
Common situations: Server URL changed (new domain, added port, HTTPS enforced) while the IdP still stamps the old URL; dev assertion pointed at localhost presented against a deployed server; reverse proxy terminating TLS so the configured server URL differs from the one signed.
Related errors
- Assertion is missing resource claim
- OAuth client not found - cached credentials may be stale
- OAuth server rejected the static client credentials. Verify
- Assertion must include exp claim
- Assertion must include sub claim
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/cc7418c9edb1565a.
Report an issue: GitHub.