PrefectHQ/fastmcp · error · IdentityAssertionError
Assertion must include exp claim
Error message
Assertion must include exp claim
What it means
FastMCP's identity assertion validator (ID-JAG / RFC 7523 JWT assertion flow) requires every assertion JWT to carry a numeric `exp` (expiration) claim. The JWT verifier's own expiry check can pass trivially when exp is absent, so `validate` performs an explicit post-verification check and raises `IdentityAssertionError` if `exp` is missing or is not a numeric date. This enforces RFC 7523 §3, which mandates exp on client assertions.
Source
Thrown at fastmcp_slim/fastmcp/server/auth/identity_assertion.py:389
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)"
)
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")View on GitHub (pinned to 1f02114297)
Solutions
- Configure the assertion issuer (IdP or token minting code) to always include `exp` as a NumericDate (epoch seconds) in the JWT payload.
- If minting assertions yourself, add `exp: int(time.time()) + lifetime` to the claims dict before signing.
- Check the issuer's JWT library settings — some disable default claim injection; enable expiry claims.
- Decode the assertion (e.g. jwt.io or `decode_jwt` utilities) to confirm exp is present and numeric.
Example fix
// before
claims = {"iss": issuer, "sub": user, "aud": server_url, "iat": now}
// after
claims = {"iss": issuer, "sub": user, "aud": server_url, "iat": now, "exp": now + 300} Defensive patterns
Strategy: validation
Validate before calling
import time
def has_valid_exp(claims: dict) -> bool:
exp = claims.get("exp")
return isinstance(exp, (int, float)) and not isinstance(exp, bool) and exp > time.time() Type guard
def is_numeric_date(v) -> bool:
return isinstance(v, (int, float)) and not isinstance(v, bool) Try / catch
try:
token = await exchange(assertion)
except IdentityAssertionError as e:
if "exp claim" in str(e):
assertion = mint_assertion(include_exp=True)
token = await exchange(assertion)
else:
raise Prevention
- Always set exp when minting assertion JWTs; make it part of a shared claims-builder helper.
- Assert in tests that every minted assertion contains numeric exp/iat/jti/sub.
- Validate assertions locally with a JWT decoder before sending them in a token exchange.
When it happens
Trigger: Calling `IdentityAssertionMiddleware.validate()` (via an OAuth token exchange presenting an ID-JAG assertion) with a signed JWT whose payload omits the `exp` claim, or encodes `exp` as a string (e.g. "2026-01-01") or null rather than a NumericDate, so `_numeric_date_claim(claims, "exp")` returns None.
Common situations: Custom or misconfigured token issuers minting identity assertion JWTs without exp; hand-rolled JWT construction in tests that only sets iss/sub/aud; an issuer serializing exp as an ISO string instead of epoch seconds.
Related errors
- Assertion must include sub claim
- Assertion must include a string jti claim
- Assertion is not yet valid (nbf in future)
- Assertion iat is in the future
- Assertion lifetime too long (max {self.MAX_ASSERTION_LIFETIM
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/3ea77c775eb06b9e.
Report an issue: GitHub.