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

  1. Update the issuer's resource-indicator config to emit this server's exact public URL.
  2. Restart/re-authenticate so newly minted assertions carry the current server URL.
  3. Compare the two URLs in the error message and reconcile scheme, host, port, and path differences.
  4. 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

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


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/cc7418c9edb1565a. Report an issue: GitHub.