PrefectHQ/fastmcp · error · AuthenticationError

Invalid client assertion: {e}

Error message

Invalid client assertion: {e}

What it means

This AuthenticationError wraps a ValueError from the CIMD manager's validate_private_key_jwt. The client sent a JWT assertion for private_key_jwt auth, but the JWT failed validation — bad signature, wrong audience, expired, malformed, or signed by a key not in the client's published jwks. The original ValueError is chained as __cause__ for debugging.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/auth.py:292

            assertion = form_data.get("client_assertion")

            if assertion_type != JWT_BEARER_ASSERTION_TYPE:
                raise AuthenticationError(
                    f"Invalid client_assertion_type: expected {JWT_BEARER_ASSERTION_TYPE}"
                )

            if not assertion or not isinstance(assertion, str):
                raise AuthenticationError("Missing client_assertion")

            # Validate the JWT assertion using CIMD manager
            try:
                await self._cimd_manager.validate_private_key_jwt(
                    assertion=assertion,
                    client=client,
                    token_endpoint=self._token_endpoint_url,
                )
            except ValueError as e:
                raise AuthenticationError(f"Invalid client assertion: {e}") from e

            return client

        # Delegate to SDK for other authentication methods
        return await super().authenticate_request(request)


class AuthProvider(TokenVerifierProtocol):
    """Base class for all FastMCP authentication providers.

    This class provides a unified interface for all authentication providers,
    whether they are simple token verifiers or full OAuth authorization servers.
    All providers must be able to verify tokens and can optionally provide
    custom authentication routes.
    """

    def __init__(
        self,

View on GitHub (pinned to 1f02114297)

Solutions

  1. Inspect the chained ValueError (e.__cause__) to identify the exact failure: signature, audience, exp, or format.
  2. Sign the assertion with the private key matching a key published in the client's CIMD jwks, using an asymmetric algorithm (RS256/ES256).
  3. Set the JWT 'aud' claim to the server's token endpoint URL exactly as advertised in server metadata.
  4. Issue a fresh assertion with iat/exp within the accepted window and check for clock skew (NTP).
  5. Update the hosted CIMD document if the client's signing keys changed.

Example fix

// before (wrong audience)
claims = {'iss': client_id, 'sub': client_id, 'aud': server_base_url, 'exp': exp}
// after
claims = {'iss': client_id, 'sub': client_id, 'aud': token_endpoint_url, 'exp': exp}
Defensive patterns

Strategy: try-catch

Validate before calling

import time
claims = {'iss': client_id, 'sub': client_id, 'aud': token_endpoint_url, 'iat': int(time.time()), 'exp': int(time.time()) + 300}
assert claims['aud'] == token_endpoint_url
assert claims['exp'] > time.time()

Try / catch

try:
    client = await auth.authenticate_request(request)
except AuthenticationError as e:
    logger.warning('private_key_jwt rejected: %s', e.__cause__)
    return JSONResponse({'error': 'invalid_client'}, status_code=401)

Prevention

When it happens

Trigger: POST to the token endpoint with client_assertion present for a private_key_jwt CIMD client, but the JWT has an invalid signature against the client's jwks, targets the wrong audience (not the token endpoint URL), is expired or issued in the future, uses an unsupported algorithm, or is structurally malformed.

Common situations: Clock skew between client and server making the JWT appear expired; the client rotated keys but the CIMD jwks still lists old keys; audience set to the server base URL instead of the token endpoint; signing with HS256 instead of an asymmetric algorithm; assertion reused after exp.

Related errors


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