PrefectHQ/fastmcp · critical · ValueError

jwt_signing_key is required when upstream_client_secret is n

Error message

jwt_signing_key is required when upstream_client_secret is not provided. The JWT signing key cannot be derived without a client secret.

What it means

OAuthProxy requires a JWT signing key to sign issued tokens. If no explicit jwt_signing_key is given, it is derived deterministically from upstream_client_secret; with neither provided, derivation is impossible and __init__ raises ValueError. This fails fast at startup rather than producing unsigned or randomly-keyed tokens.

Source

Thrown at fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py:559

        self._extra_token_params: dict[str, str] = extra_token_params or {}

        # Token expiry fallback (None means use smart default based on refresh token)
        self._fallback_access_token_expiry_seconds: int | None = (
            fallback_access_token_expiry_seconds
        )
        self._fallback_refresh_token_expiry_seconds: int = (
            fallback_refresh_token_expiry_seconds
            if fallback_refresh_token_expiry_seconds is not None
            else DEFAULT_REFRESH_TOKEN_EXPIRY_SECONDS
        )
        self._fastmcp_access_token_expiry_seconds: int | None = (
            fastmcp_access_token_expiry_seconds
        )
        self._token_expiry_threshold_seconds: int = token_expiry_threshold_seconds

        if jwt_signing_key is None:
            if upstream_client_secret is None:
                raise ValueError(
                    "jwt_signing_key is required when upstream_client_secret is not provided. "
                    "The JWT signing key cannot be derived without a client secret."
                )
            jwt_signing_key = derive_jwt_key(
                high_entropy_material=upstream_client_secret,
                salt="fastmcp-jwt-signing-key",
            )

        if isinstance(jwt_signing_key, str):
            if len(jwt_signing_key) < 12:
                logger.warning(
                    "jwt_signing_key is less than 12 characters; it is recommended to use a longer. "
                    "string for the key derivation."
                )
            jwt_signing_key = derive_jwt_key(
                low_entropy_material=jwt_signing_key,
                salt="fastmcp-jwt-signing-key",
            )

View on GitHub (pinned to 1f02114297)

Solutions

  1. Pass an explicit jwt_signing_key (e.g. a stable high-entropy secret loaded from an environment variable)
  2. Provide upstream_client_secret so the key can be derived with derive_jwt_key
  3. If the upstream flow has no secret, generate and persist a dedicated signing key instead of leaving both None

Example fix

// before
proxy = OAuthProxy(upstream_authorization_endpoint=..., upstream_token_endpoint=...)  # no secret, no key
// after
proxy = OAuthProxy(
    upstream_authorization_endpoint=...,
    upstream_token_endpoint=...,
    jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
)
Defensive patterns

Strategy: validation

Validate before calling

jwt_key = os.environ.get("FASTMCP_JWT_SIGNING_KEY")
client_secret = os.environ.get("UPSTREAM_CLIENT_SECRET")
if jwt_key is None and client_secret is None:
    raise SystemExit("Set FASTMCP_JWT_SIGNING_KEY or UPSTREAM_CLIENT_SECRET before starting")

Type guard

def has_signing_material(key: str | None, secret: str | None) -> bool:
    return key is not None or secret is not None

Try / catch

try:
    proxy = OAuthProxy(..., jwt_signing_key=key, upstream_client_secret=secret)
except ValueError as e:
    if "jwt_signing_key is required" in str(e):
        raise SystemExit("Configuration error: provide jwt_signing_key or upstream_client_secret") from e
    raise

Prevention

When it happens

Trigger: Constructing OAuthProxy with jwt_signing_key=None and upstream_client_secret=None (or omitted).

Common situations: Configuring the proxy against an IdP flow that doesn't use a client secret (e.g. public-client/PKCE setups) and forgetting to supply a signing key; refactoring constructor arguments and dropping the secret; missing env var not passed into the constructor.

Related errors


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