PrefectHQ/fastmcp · error · IdentityAssertionError

Assertion missing required scopes: {sorted(missing)}

Error message

Assertion missing required scopes: {sorted(missing)}

What it means

The identity assertion config declares `required_scopes`, and the assertion's scope claims (scope/scope arrays via `_assertion_scopes`) do not grant all of them. The middleware derives the issued token's scopes from the assertion, so a missing required scope means the request cannot be authorized and is rejected with the sorted list of missing scopes.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:414

                raise IdentityAssertionError(
                    f"Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s)"
                )
        elif exp > now + self.MAX_ASSERTION_LIFETIME:
            raise IdentityAssertionError(
                f"Assertion exp too far in future (max {self.MAX_ASSERTION_LIFETIME}s)"
            )

        # 4. sub is mandatory (RFC 7523 §3) — it identifies the end user.
        sub = claims.get("sub")
        if not sub:
            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")

View on GitHub (pinned to 1f02114297)

Solutions

  1. Update the issuer/IdP client configuration to grant all required scopes in the assertion.
  2. Have the user re-authenticate/re-consent so the new scopes appear in issued assertions.
  3. Align scope names exactly between config.required_scopes and what the issuer emits.
  4. As a last resort, the server operator can trim `required_scopes` in the config to what the issuer legitimately provides.

Example fix

// before
claims = {"scope": "read", ...}  # server requires ["read", "write"]
// after
claims = {"scope": "read write", ...}
Defensive patterns

Strategy: validation

Validate before calling

def scopes_satisfied(claims: dict, required: set[str]) -> bool:
    granted = set()
    scope = claims.get("scope") or claims.get("scopes") or []
    if isinstance(scope, str):
        granted = set(scope.split())
    elif isinstance(scope, list):
        granted = set(scope)
    return required.issubset(granted)

Type guard

def grants_scopes(claims: dict, required: set[str]) -> bool:
    s = claims.get("scope", "")
    return isinstance(s, str) and required.issubset(set(s.split()))

Try / catch

try:
    token = await exchange(assertion)
except IdentityAssertionError as e:
    if "missing required scopes" in str(e):
        assertion = await obtain_assertion(request_scopes=REQUIRED_SCOPES)  # re-consent flow
        token = await exchange(assertion)
    else:
        raise

Prevention

When it happens

Trigger: Calling `validate()` with an assertion whose granted scope set is a strict subset of `config.required_scopes` — e.g. config requires ["read", "write"] but the assertion only carries "read".

Common situations: Server tightened `required_scopes` in the IdentityAssertionConfig without updating the issuing IdP's client scopes; user consent at the IdP omitted a scope; scope naming mismatches (e.g. `read:data` vs `read`).

Related errors


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