PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion failed signature/issuer/audience/expiry validation
Error message
Assertion failed signature/issuer/audience/expiry validation
What it means
After selecting a verifier for the trusted issuer, JWTVerifier.load_access_token fully validated the assertion (signature against the issuer's JWKS, iss, aud, exp) and returned None, meaning cryptographic or standard-claim validation failed. FastMCP collapses all of these into one message to avoid leaking verification details to callers.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:379
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)")
if iat is not None:
if iat > now + self.CLOCK_SKEW_SECONDS:
raise IdentityAssertionError("Assertion iat is in the future")
if exp - iat > self.MAX_ASSERTION_LIFETIME:
raise IdentityAssertionError(
f"Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIME}s)"View on GitHub (pinned to 1f02114297)
Solutions
- Check server clock synchronization (NTP) and that the assertion's exp is in the future.
- Confirm the audience: the assertion's aud must match what the server/exchange expects; fix client aud configuration if wrong.
- Have the client re-obtain a fresh assertion via the id-jag exchange instead of reusing a cached/expired one.
- If keys were rotated, clear/restart so the verifier refetches JWKS, and verify the token with the IdP's debugger to confirm the signature.
Example fix
// before: reusing a cached assertion across hours assertion = cached_assertion_from_yesterday // after: perform a fresh id-jag exchange before each authorization-grant exchange assertion = await client.exchange_id_token_for_assertion(id_token)
Defensive patterns
Strategy: try-catch
Validate before calling
import time, base64, json
claims = json.loads(base64.urlsafe_b64decode(assertion.split('.')[1] + '=='))
assert claims.get('exp', 0) > time.time(), 'assertion expired'
assert claims.get('aud') == EXPECTED_AUDIENCE, 'audience mismatch' Type guard
def assertion_is_current(token: str, aud: str, skew: int = 60) -> bool:
import base64, json, time
c = json.loads(base64.urlsafe_b64decode(token.split('.')[1] + '=='))
return c.get('exp', 0) + skew > time.time() and aud in (c.get('aud') or []) Try / catch
try:
await provider.validate(assertion)
except IdentityAssertionError as e:
if 'signature/issuer/audience/expiry' in str(e):
# get a fresh assertion; do not retry with the same token
assertion = await obtain_fresh_assertion()
else:
raise Prevention
- Sync server clocks with NTP
- Always fetch a fresh assertion instead of caching expired ones
- Confirm aud configuration matches between client and server
- Restart/refetch JWKS after IdP key rotation
- Validate tokens with the IdP's debugger when signature checks fail
When it happens
Trigger: validate() on an assertion that is structurally fine and from a trusted issuer, but whose signature doesn't verify against the issuer's current JWKS, whose aud doesn't match the expected audience, whose exp is past, or whose iss inside verification disagrees.
Common situations: Clock skew between IdP and server making exp seem passed; token expired after client cached it; IdP rotated signing keys and the server cached stale JWKS; audience/audience (client_id/resource) misconfiguration; token signed by a different key/tenant than expected.
Related errors
- Assertion replay detected: jti {jti} already used
- Untrusted assertion issuer: {iss!r}
- The CLI state lock must not be a symbolic link
- CLI state must not be a symbolic link: {path.name}
- Functions with *args are not supported as resource templates
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/2fd3897404e449cc.
Report an issue: GitHub.