{"record":{"id":"a822d3294a9fd200","repo":"PrefectHQ/fastmcp","slug":"untrusted-assertion-issuer-iss-r","errorCode":null,"errorMessage":"Untrusted assertion issuer: {iss!r}","messagePattern":"Untrusted assertion issuer: (.+?)","errorType":"exception","errorClass":"IdentityAssertionError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/server/auth/identity_assertion.py","lineNumber":373,"sourceCode":"        if not isinstance(header, dict):\n            # A JSON-array/scalar header is valid JSON but not a JOSE header;\n            # guard before .get() so this maps to invalid_grant, not a 500.\n            raise IdentityAssertionError(\"Assertion JOSE header must be a JSON object\")\n        if header.get(\"typ\") != ID_JAG_TYP:\n            raise IdentityAssertionError(\n                f\"Assertion typ must be {ID_JAG_TYP!r}, got {header.get('typ')!r}\"\n            )\n\n        # 2. iss must be a trusted issuer before we fetch any keys for it.\n        try:\n            unverified_claims = _decode_unverified_claims(assertion)\n        except (ValueError, KeyError, IndexError) as e:\n            raise IdentityAssertionError(f\"Malformed assertion payload: {e}\") from e\n        if not isinstance(unverified_claims, dict):\n            raise IdentityAssertionError(\"Assertion payload is not a JSON object\")\n        iss = unverified_claims.get(\"iss\")\n        if not iss or iss not in self.config.trusted_issuers:\n            raise IdentityAssertionError(f\"Untrusted assertion issuer: {iss!r}\")\n\n        # 3. Verify signature, iss, aud, and exp via JWTVerifier.\n        verifier = await self._get_verifier(iss)\n        access_token = await verifier.load_access_token(assertion)\n        if access_token is None:\n            raise IdentityAssertionError(\n                \"Assertion failed signature/issuer/audience/expiry validation\"\n            )\n        claims = access_token.claims\n\n        now = time.time()\n        exp = _numeric_date_claim(claims, \"exp\")\n        iat = _numeric_date_claim(claims, \"iat\")\n        nbf = _numeric_date_claim(claims, \"nbf\")\n        if exp is None:\n            raise IdentityAssertionError(\"Assertion must include exp claim\")\n        if nbf is not None and nbf > now + self.CLOCK_SKEW_SECONDS:\n            raise IdentityAssertionError(\"Assertion is not yet valid (nbf in future)\")","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/server/auth/identity_assertion.py#L355-L391","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconfig = IdentityAssertionConfig(trusted_issuers={\"https://idp.example.com\"})\n// after (match the issuer the IdP actually puts in tokens)\nconfig = IdentityAssertionConfig(trusted_issuers={\"https://idp.example.com/realms/main\"})","handlingStrategy":"validation","validationCode":"import base64, json\nclaims = json.loads(base64.urlsafe_b64decode(assertion.split('.')[1] + '=='))\niss = claims.get('iss')\nassert iss in TRUSTED_ISSUERS, f'issuer {iss!r} not configured server-side'","typeGuard":"def is_trusted_issuer(iss) -> bool:\n    return isinstance(iss, str) and iss in TRUSTED_ISSUERS","tryCatchPattern":"try:\n    await provider.validate(assertion)\nexcept IdentityAssertionError as e:\n    if 'Untrusted assertion issuer' in str(e):\n        log.warning('assertion from unconfigured issuer: %s', e)\n    raise","preventionTips":["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"],"tags":["jwt","issuer","configuration","security"],"backgroundTag":"untrusted-jwt-issuer","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}