PrefectHQ/fastmcp · error · ValueError

Invalid JWT assertion

Error message

Invalid JWT assertion

What it means

validate_assertion verifies the private_key_jwt client assertion's signature, expiration, issuer, and audience via JWTVerifier. If the assertion fails any of these checks, load_access_token returns None and a ValueError('Invalid JWT assertion') is raised — the client presented a malformed, wrongly signed, expired, or wrongly-addressed JWT.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/cimd.py:585

                    del self._verifier_cache[oldest_key]
                self._verifier_cache[cache_key] = verifier
        elif cimd_doc.jwks:
            # Inline JWKS — no caching since the key is embedded
            public_key = self._extract_public_key_from_jwks(assertion, cimd_doc.jwks)
            verifier = _JWTVerifier(
                public_key=public_key,
                issuer=client_id,
                audience=token_endpoint,
            )
        else:
            raise ValueError(
                "CIMD document must have jwks_uri or jwks for private_key_jwt"
            )

        # 2. Verify JWT using JWTVerifier (handles signature, exp, iss, aud)
        access_token = await verifier.load_access_token(assertion)
        if not access_token:
            raise ValueError("Invalid JWT assertion")

        claims = access_token.claims

        # 3. Validate assertion lifetime (exp and iat)
        now = time.time()
        exp = claims.get("exp")
        iat = claims.get("iat")

        if not exp:
            raise ValueError("Assertion must include exp claim")

        # Validate exp is in the future (with small clock skew tolerance)
        if exp < now - 30:  # 30 second clock skew tolerance
            raise ValueError("Assertion has expired")

        # If iat is present, validate it and check assertion lifetime
        if iat:
            if iat > now + 30:  # 30 second clock skew tolerance

View on GitHub (pinned to 1f02114297)

Solutions

  1. Sign the assertion with the private key corresponding to a key published in the document's jwks/jwks_uri
  2. Set 'iss' to the client_id and 'aud' to the FastMCP token endpoint URL exactly
  3. Check system clocks and keep assertion lifetime short (exp within a few minutes)
  4. Decode the assertion locally (without verification) to compare iss/aud/exp claims against expectations
  5. Catch ValueError and respond with OAuth invalid_client / invalid_grant per the token endpoint contract

Example fix

# before
claims = {"iss": "https://app.example.com", "aud": "https://api.example.com", ...}
# after
claims = {
    "iss": client_id,
    "aud": "https://mcp.example.com/token",
    "sub": client_id,
    "exp": int(time.time()) + 300,
    "jti": str(uuid4()),
}
Defensive patterns

Strategy: try-catch

Validate before calling

import time, jwt  # pre-check on the client before sending the assertion
claims = {"iss": client_id, "sub": client_id, "aud": token_endpoint,
          "exp": int(time.time()) + 300, "jti": str(uuid4())}
assertion = jwt.encode(claims, private_key, algorithm="RS256", headers={"kid": kid})

Type guard

def assertion_shape_ok(token: str) -> bool:
    parts = token.split(".")
    return len(parts) == 3 and all(parts)

Try / catch

try:
    await manager.validate_private_key_jwt(doc, assertion, token_endpoint)
except ValueError:
    raise InvalidClientError("invalid client assertion") from None

Prevention

When it happens

Trigger: validate_private_key_jwt called with an assertion JWT that: is signed by a key not in the client's JWKS, has a mismatched 'iss' (must equal the client_id), has 'aud' not matching the token endpoint, is expired, or is structurally malformed.

Common situations: Client signing with a rotated-out key while the JWKS still lists (or no longer lists) the right key; 'aud' set to a resource server instead of the token endpoint URL; clock skew causing exp/nbf failures; 'iss' not exactly equal to the client_id URL; libraries issuing JWS instead of a proper JWT with claims.

Related errors


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