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
- Add the exact issuer string from the assertion to trusted_issuers in IdentityAssertionConfig (compare byte-for-byte, including scheme, host, port, trailing slash).
- Decode the assertion payload and print iss to see the exact value the client's IdP claims.
- Fix the client's IdP configuration if it points at the wrong issuer.
- 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
- Copy the issuer string byte-for-byte from the IdP's discovery document into trusted_issuers
- Watch for trailing-slash and http/https mismatches
- Re-check trusted_issuers after IdP migrations
- Keep issuer allow-lists in configuration, not code
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
- Assertion replay detected: jti {jti} already used
- Assertion failed signature/issuer/audience/expiry validation
- Invalid token issuer
- Invalid token audience
- jwt_signing_key is required when upstream_client_secret is n
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/a822d3294a9fd200.
Report an issue: GitHub.