mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
* Add M2M client credentials auth providers Wrap the SDK's client_credentials and private_key_jwt OAuth providers as FastMCP-idiomatic ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider, enabling browser-free client authentication via Client(auth=...). * Fix M2M token cache collision and explicit-scope drop Namespace the token cache by client_id so distinct clients sharing one store don't overwrite each other's tokens; pin caller-supplied scopes so the token request keeps them; fix CodeQL URL-substring check in tests; drop unused logger. * Preserve step-up scope union, scope-aware token cache, restore token expiry Only pin the caller's explicit scopes on initial authorization, leaving the SDK's step-up scope union intact; namespace the token cache by requested scopes as well as client_id; restore persisted absolute expiry on init so an expired stored token is re-fetched. * Skip expiry restore for non-expiring reloaded tokens * Distinguish expires_in=0 from omitted when restoring expiry * Scope step-up flag to the flow via ContextVar; runnable JWT signing example
89 lines
5 KiB
Text
89 lines
5 KiB
Text
---
|
|
title: Machine-to-Machine Authentication
|
|
sidebarTitle: Client Credentials
|
|
description: Authenticate your FastMCP client to a protected server without a browser.
|
|
icon: robot
|
|
---
|
|
|
|
import { VersionBadge } from "/snippets/version-badge.mdx"
|
|
|
|
<VersionBadge version="4.0.0" />
|
|
|
|
<Tip>
|
|
Machine-to-machine authentication is only relevant for HTTP-based transports.
|
|
</Tip>
|
|
|
|
When a FastMCP client runs without a human present — a backend service, a scheduled job, a CI pipeline, one MCP server calling another — it cannot complete the browser-based [OAuth](/clients/auth/oauth) flow. Instead it authenticates as itself using the OAuth 2.0 **client credentials** grant: the client presents its own credentials directly to the authorization server, receives an access token, and attaches that token to every request. There is no redirect, no consent screen, and no user.
|
|
|
|
FastMCP provides two providers for this, both implementing the `httpx2.Auth` interface so they drop into the same `auth=` parameter as every other client auth option. You pass the **MCP server URL**, not a token endpoint — the token endpoint is discovered from the server's OAuth metadata, exactly as the interactive `OAuth` helper does. As with `OAuth`, you can omit the URL entirely and let the transport supply it.
|
|
|
|
## Client ID and Secret
|
|
|
|
The common case is a pre-registered client with an ID and a secret. Use `ClientCredentialsOAuthProvider` and pass it to the `auth` parameter of your `Client` or transport:
|
|
|
|
```python {2, 4-8, 10}
|
|
from fastmcp import Client
|
|
from fastmcp.client.auth import ClientCredentialsOAuthProvider
|
|
|
|
auth = ClientCredentialsOAuthProvider(
|
|
client_id="my-client-id",
|
|
client_secret="my-client-secret",
|
|
scopes=["read", "write"],
|
|
)
|
|
|
|
async with Client("https://example.com/mcp", auth=auth) as client:
|
|
await client.list_tools()
|
|
```
|
|
|
|
The provider discovers the authorization server, exchanges the credentials for an access token, and caches the token in memory for the life of the client. When the token expires it is re-acquired automatically on the next request. Because re-acquiring a token is a single non-interactive request, tokens are held in memory by default with no warning — unlike the interactive `OAuth` flow, losing the cache on restart costs nothing.
|
|
|
|
### `ClientCredentialsOAuthProvider` Parameters
|
|
|
|
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)` — the transport supplies the URL automatically.
|
|
- **`client_id`** (`str`, required): The pre-registered OAuth client ID.
|
|
- **`client_secret`** (`str`, required): The OAuth client secret.
|
|
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
|
|
- **`token_endpoint_auth_method`** (`"client_secret_basic" | "client_secret_post"`, optional): How the credentials are presented to the token endpoint. Defaults to `"client_secret_basic"` (an HTTP Basic `Authorization` header); use `"client_secret_post"` to send them in the request body instead.
|
|
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
|
|
|
|
## Private Key JWT
|
|
|
|
Some authorization servers require the client to prove its identity with a signed JWT assertion (RFC 7523 `private_key_jwt`) instead of a shared secret. This is common with workload identity federation, where the assertion comes from a cloud identity provider. Use `PrivateKeyJWTOAuthProvider` and supply an `assertion_provider` — an async callback that receives the authorization server's issuer identifier (the required JWT audience) and returns the assertion.
|
|
|
|
For a locally signed assertion, build the callback with `SignedJWTParameters`:
|
|
|
|
```python {4-7, 9, 11-15, 17-20, 22}
|
|
from pathlib import Path
|
|
|
|
from fastmcp import Client
|
|
from fastmcp.client.auth import (
|
|
PrivateKeyJWTOAuthProvider,
|
|
SignedJWTParameters,
|
|
)
|
|
|
|
private_key_pem = Path("client-signing-key.pem").read_text()
|
|
|
|
jwt_params = SignedJWTParameters(
|
|
issuer="my-client-id",
|
|
subject="my-client-id",
|
|
signing_key=private_key_pem,
|
|
)
|
|
|
|
auth = PrivateKeyJWTOAuthProvider(
|
|
client_id="my-client-id",
|
|
assertion_provider=jwt_params.create_assertion_provider(),
|
|
)
|
|
|
|
async with Client("https://example.com/mcp", auth=auth) as client:
|
|
await client.list_tools()
|
|
```
|
|
|
|
If you already have a JWT from an identity provider, wrap it with `static_assertion_provider`, or pass your own `async def provider(audience: str) -> str` callback to fetch one on demand.
|
|
|
|
### `PrivateKeyJWTOAuthProvider` Parameters
|
|
|
|
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)`.
|
|
- **`client_id`** (`str`, required): The OAuth client ID.
|
|
- **`assertion_provider`** (`Callable[[str], Awaitable[str]]`, required): Async callback that receives the authorization server's issuer identifier and returns a signed JWT assertion.
|
|
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
|
|
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
|