PrefectHQ/fastmcp · error · IdentityAssertionError

Untrusted assertion issuer: {iss!r}

Error message

Untrusted assertion issuer: {iss!r}

What it means

The assertion's unverified `iss` claim is missing or is not in the server's configured trusted_issuers set. FastMCP only fetches keys and verifies assertions from issuers the operator explicitly trusts, preventing SSRF-style key fetches against arbitrary issuers.

Source

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

        if not isinstance(header, dict):
            # A JSON-array/scalar header is valid JSON but not a JOSE header;
            # guard before .get() so this maps to invalid_grant, not a 500.
            raise IdentityAssertionError("Assertion JOSE header must be a JSON object")
        if header.get("typ") != ID_JAG_TYP:
            raise IdentityAssertionError(
                f"Assertion typ must be {ID_JAG_TYP!r}, got {header.get('typ')!r}"
            )

        # 2. iss must be a trusted issuer before we fetch any keys for it.
        try:
            unverified_claims = _decode_unverified_claims(assertion)
        except (ValueError, KeyError, IndexError) as e:
            raise IdentityAssertionError(f"Malformed assertion payload: {e}") from e
        if not isinstance(unverified_claims, dict):
            raise IdentityAssertionError("Assertion payload is not a JSON object")
        iss = unverified_claims.get("iss")
        if not iss or iss not in self.config.trusted_issuers:
            raise IdentityAssertionError(f"Untrusted assertion issuer: {iss!r}")

        # 3. Verify signature, iss, aud, and exp via JWTVerifier.
        verifier = await self._get_verifier(iss)
        access_token = await verifier.load_access_token(assertion)
        if access_token is None:
            raise IdentityAssertionError(
                "Assertion failed signature/issuer/audience/expiry validation"
            )
        claims = access_token.claims

        now = time.time()
        exp = _numeric_date_claim(claims, "exp")
        iat = _numeric_date_claim(claims, "iat")
        nbf = _numeric_date_claim(claims, "nbf")
        if exp is None:
            raise IdentityAssertionError("Assertion must include exp claim")
        if nbf is not None and nbf > now + self.CLOCK_SKEW_SECONDS:
            raise IdentityAssertionError("Assertion is not yet valid (nbf in future)")

View on GitHub (pinned to 1f02114297)

Solutions

  1. Add the exact issuer string from the assertion to trusted_issuers in IdentityAssertionConfig (compare byte-for-byte, including scheme, host, port, trailing slash).
  2. Decode the assertion payload and print iss to see the exact value the client's IdP claims.
  3. Fix the client's IdP configuration if it points at the wrong issuer.
  4. Restart/redeploy the server after updating the configuration.

Example fix

// before
config = IdentityAssertionConfig(trusted_issuers={"https://idp.example.com"})
// after (match the issuer the IdP actually puts in tokens)
config = IdentityAssertionConfig(trusted_issuers={"https://idp.example.com/realms/main"})
Defensive patterns

Strategy: validation

Validate before calling

import base64, json
claims = json.loads(base64.urlsafe_b64decode(assertion.split('.')[1] + '=='))
iss = claims.get('iss')
assert iss in TRUSTED_ISSUERS, f'issuer {iss!r} not configured server-side'

Type guard

def is_trusted_issuer(iss) -> bool:
    return isinstance(iss, str) and iss in TRUSTED_ISSUERS

Try / catch

try:
    await provider.validate(assertion)
except IdentityAssertionError as e:
    if 'Untrusted assertion issuer' in str(e):
        log.warning('assertion from unconfigured issuer: %s', e)
    raise

Prevention

When it happens

Trigger: validate() receives an assertion whose payload's iss is absent, empty, or not a key of config.trusted_issuers — checked before any JWKS/discovery fetch.

Common situations: Operator forgot to add the IdP's issuer URL to trusted_issuers (or it differs by trailing slash/https-vs-http); a client using a different IdP than the one configured; issuer mismatch after migrating IdP domains; case/path variations in the issuer identifier.

Related errors


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