mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
Add JWT audience validation and RFC 8707 warnings to auth providers (#3204)
* Add JWT audience validation and RFC 8707 warnings to auth providers * chore: Update SDK documentation * Update AuthKit example README env var name * Move RFC 8707 warnings inside default verifier guard * chore: Update SDK documentation --------- Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
This commit is contained in:
parent
016b9f90e0
commit
a8b100eb8c
14 changed files with 78 additions and 48 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -70,6 +70,7 @@ dmypy.json
|
|||
/PLAN.md
|
||||
/TODO.md
|
||||
/STATUS.md
|
||||
plans/
|
||||
|
||||
# Common FastMCP test files
|
||||
/test.py
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
|
|||
|
||||
This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where AuthKit handles user login and your FastMCP server validates the tokens.
|
||||
|
||||
<Warning>
|
||||
AuthKit does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server. If you need resource-specific audience validation, consider using [WorkOSProvider](/integrations/workos) (OAuth proxy pattern) instead.
|
||||
</Warning>
|
||||
|
||||
## Configuration
|
||||
### Prerequisites
|
||||
|
|
@ -62,21 +65,23 @@ To test your server, you can use the `fastmcp` CLI to run it locally. Assuming y
|
|||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Now, you can use a FastMCP client to test that you can reach your server after authenticating:
|
||||
AuthKit defaults DCR clients to `client_secret_basic` for token exchange, which conflicts with how some MCP clients send credentials. To avoid token exchange errors, register as a public client by setting `token_endpoint_auth_method` to `"none"`:
|
||||
|
||||
```python
|
||||
```python client.py
|
||||
from fastmcp import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
import asyncio
|
||||
|
||||
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
|
||||
|
||||
async def main():
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
async with Client("http://localhost:8000/mcp", auth=auth) as client:
|
||||
assert await client.ping()
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
|
||||
## Production Configuration
|
||||
|
||||
For production deployments, load sensitive configuration from environment variables:
|
||||
|
|
@ -93,4 +98,4 @@ auth = AuthKitProvider(
|
|||
)
|
||||
|
||||
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
|
||||
```
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,21 +11,43 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
|
|||
|
||||
This guide shows you how to secure your FastMCP server using **Supabase Auth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Supabase handles user authentication and your FastMCP server validates the tokens.
|
||||
|
||||
<Warning>
|
||||
Supabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server.
|
||||
</Warning>
|
||||
|
||||
## Consent UI Requirement
|
||||
|
||||
Supabase's OAuth Server delegates the user consent screen to your application. When an MCP client initiates authorization, Supabase authenticates the user and then redirects to your application at a configured callback URL (e.g., `https://your-app.com/oauth/callback?authorization_id=...`). Your application must host a page that calls Supabase's `approveAuthorization()` or `denyAuthorization()` APIs to complete the flow.
|
||||
|
||||
`SupabaseProvider` handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before you begin, you will need:
|
||||
1. A **[Supabase Account](https://supabase.com/)** with a project or a self-hosted **Supabase Auth** instance
|
||||
2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)
|
||||
3. **Dynamic Client Registration enabled** in the same settings
|
||||
4. A **consent UI** hosted at your configured authorization path (see above)
|
||||
5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
|
||||
|
||||
### Step 1: Get Supabase Project URL
|
||||
### Step 1: Enable Supabase OAuth Server
|
||||
|
||||
In your Supabase Dashboard:
|
||||
1. Go to **Authentication → OAuth Server**
|
||||
2. Enable the **OAuth Server**
|
||||
3. Set your **Site URL** to where your consent UI is hosted
|
||||
4. Set the **Authorization Path** (e.g., `/oauth/callback`)
|
||||
5. Enable **Allow Dynamic OAuth Apps** for MCP client registration
|
||||
|
||||
### Step 2: Get Supabase Project URL
|
||||
|
||||
In your Supabase Dashboard:
|
||||
1. Go to **Project Settings**
|
||||
2. Copy your **Project URL** (e.g., `https://abc123.supabase.co`)
|
||||
|
||||
### Step 2: FastMCP Configuration
|
||||
### Step 3: FastMCP Configuration
|
||||
|
||||
Create your FastMCP server using the `SupabaseProvider`:
|
||||
|
||||
|
|
@ -33,12 +55,9 @@ Create your FastMCP server using the `SupabaseProvider`:
|
|||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
||||
|
||||
# Configure Supabase Auth
|
||||
auth = SupabaseProvider(
|
||||
project_url="https://abc123.supabase.co",
|
||||
base_url="http://localhost:8000",
|
||||
# Optional: customize auth_route for self-hosted Supabase Auth with custom routes
|
||||
# auth_route="/my/auth/route"
|
||||
)
|
||||
|
||||
mcp = FastMCP("Supabase Protected Server", auth=auth)
|
||||
|
|
@ -62,8 +81,6 @@ Start your FastMCP server with HTTP transport to enable OAuth flows:
|
|||
fastmcp run server.py --transport http --port 8000
|
||||
```
|
||||
|
||||
Your server is now running and protected by Supabase authentication.
|
||||
|
||||
### Testing with a Client
|
||||
|
||||
Create a test client that authenticates with your Supabase-protected server:
|
||||
|
|
@ -73,12 +90,9 @@ from fastmcp import Client
|
|||
import asyncio
|
||||
|
||||
async def main():
|
||||
# The client will automatically handle Supabase OAuth
|
||||
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
|
||||
# First-time connection will open Supabase login in your browser
|
||||
print("✓ Authenticated with Supabase!")
|
||||
print("Authenticated with Supabase!")
|
||||
|
||||
# Test the protected tool
|
||||
result = await client.call_tool("protected_tool", {"message": "Hello!"})
|
||||
print(result)
|
||||
|
||||
|
|
@ -87,9 +101,9 @@ if __name__ == "__main__":
|
|||
```
|
||||
|
||||
When you run the client for the first time:
|
||||
1. Your browser will open to Supabase's authorization page
|
||||
2. After you authorize, you'll be redirected back
|
||||
3. The client receives the token and can make authenticated requests
|
||||
1. Your browser will open to Supabase's authorization endpoint
|
||||
2. After authenticating, Supabase redirects to your consent UI
|
||||
3. After you approve, the client receives the token and can make authenticated requests
|
||||
|
||||
## Production Configuration
|
||||
|
||||
|
|
@ -100,11 +114,9 @@ import os
|
|||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth.providers.supabase import SupabaseProvider
|
||||
|
||||
# Load configuration from environment variables
|
||||
auth = SupabaseProvider(
|
||||
project_url=os.environ["SUPABASE_PROJECT_URL"],
|
||||
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
|
||||
auth_route=os.environ.get("SUPABASE_AUTH_ROUTE", "/auth/v1"), # Optional: for custom routes
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="Supabase Secured App", auth=auth)
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ https://docs.scalekit.com/mcp/overview/
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/scalekit.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_routes(self, mcp_path: str | None = None) -> list[Route]
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ https://supabase.com/docs/guides/auth/jwts
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/supabase.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_routes(self, mcp_path: str | None = None) -> list[Route]
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L290" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L301" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_routes(self, mcp_path: str | None = None) -> list[Route]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Demonstrates FastMCP server protection with AuthKit Dynamic Client Registration.
|
|||
1. Set your AuthKit domain:
|
||||
|
||||
```bash
|
||||
export FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN="https://your-app.authkit.app"
|
||||
export AUTHKIT_DOMAIN="https://your-app.authkit.app"
|
||||
```
|
||||
|
||||
2. Run the server:
|
||||
|
|
|
|||
|
|
@ -9,23 +9,24 @@ To run:
|
|||
import asyncio
|
||||
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.client.auth import OAuth
|
||||
|
||||
SERVER_URL = "http://127.0.0.1:8000/mcp"
|
||||
|
||||
|
||||
async def main():
|
||||
try:
|
||||
async with Client(SERVER_URL, auth="oauth") as client:
|
||||
assert await client.ping()
|
||||
print("✅ Successfully authenticated!")
|
||||
# AuthKit defaults DCR clients to client_secret_basic, which conflicts
|
||||
# with how MCP SDKs send credentials. Force "none" to register as a
|
||||
# public client and avoid token exchange errors.
|
||||
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
|
||||
async with Client(SERVER_URL, auth=auth) as client:
|
||||
assert await client.ping()
|
||||
print("Successfully authenticated!")
|
||||
|
||||
tools = await client.list_tools()
|
||||
print(f"🔧 Available tools ({len(tools)}):")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
except Exception as e:
|
||||
print(f"❌ Authentication failed: {e}")
|
||||
raise
|
||||
tools = await client.list_tools()
|
||||
print(f"Available tools ({len(tools)}):")
|
||||
for tool in tools:
|
||||
print(f" - {tool.name}: {tool.description}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ from fastmcp import FastMCP
|
|||
from fastmcp.server.auth.providers.workos import AuthKitProvider
|
||||
|
||||
auth = AuthKitProvider(
|
||||
authkit_domain=os.getenv("FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN")
|
||||
or "",
|
||||
authkit_domain=os.getenv("AUTHKIT_DOMAIN") or "",
|
||||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,13 +15,7 @@ SERVER_URL = "http://localhost:8000/mcp"
|
|||
|
||||
async def main():
|
||||
try:
|
||||
async with Client(
|
||||
SERVER_URL,
|
||||
auth=OAuth(
|
||||
# Replace with your own CIMD document URL
|
||||
client_metadata_url="https://www.jlowin.dev/mcp-client.json",
|
||||
),
|
||||
) as client:
|
||||
async with Client(SERVER_URL, auth=OAuth()) as client:
|
||||
assert await client.ping()
|
||||
print("✅ Successfully authenticated!")
|
||||
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class ScalekitProvider(RemoteAuthProvider):
|
|||
jwks_uri=f"{self.environment_url}/keys",
|
||||
issuer=self.environment_url,
|
||||
algorithm="RS256",
|
||||
audience=self.resource_id,
|
||||
required_scopes=self.required_scopes or None,
|
||||
)
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -103,10 +103,16 @@ class SupabaseProvider(RemoteAuthProvider):
|
|||
|
||||
# Create default JWT verifier if none provided
|
||||
if token_verifier is None:
|
||||
logger.warning(
|
||||
"SupabaseProvider cannot validate token audience for the specific resource "
|
||||
"because Supabase Auth does not support RFC 8707 resource indicators. "
|
||||
"This may leave the server vulnerable to cross-server token replay."
|
||||
)
|
||||
token_verifier = JWTVerifier(
|
||||
jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
|
||||
issuer=f"{self.project_url}/{self.auth_route}",
|
||||
algorithm=algorithm,
|
||||
audience="authenticated",
|
||||
required_scopes=parsed_scopes,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -252,6 +252,7 @@ class AuthKitProvider(RemoteAuthProvider):
|
|||
*,
|
||||
authkit_domain: AnyHttpUrl | str,
|
||||
base_url: AnyHttpUrl | str,
|
||||
client_id: str | None = None,
|
||||
required_scopes: list[str] | None = None,
|
||||
token_verifier: TokenVerifier | None = None,
|
||||
):
|
||||
|
|
@ -260,6 +261,9 @@ class AuthKitProvider(RemoteAuthProvider):
|
|||
Args:
|
||||
authkit_domain: Your AuthKit domain (e.g., "https://your-app.authkit.app")
|
||||
base_url: Public URL of this FastMCP server
|
||||
client_id: Your WorkOS project client ID (e.g., "client_01ABC..."). Used to
|
||||
validate the JWT audience claim. Found in your WorkOS Dashboard under
|
||||
API Keys. This is the project-level client ID, not individual MCP client IDs.
|
||||
required_scopes: Optional list of scopes to require for all requests
|
||||
token_verifier: Optional token verifier. If None, creates JWT verifier for AuthKit
|
||||
"""
|
||||
|
|
@ -273,10 +277,17 @@ class AuthKitProvider(RemoteAuthProvider):
|
|||
|
||||
# Create default JWT verifier if none provided
|
||||
if token_verifier is None:
|
||||
logger.warning(
|
||||
"AuthKitProvider cannot validate token audience for the specific resource "
|
||||
"because AuthKit does not support RFC 8707 resource indicators. "
|
||||
"This may leave the server vulnerable to cross-server token replay. "
|
||||
"Consider using WorkOSProvider (OAuth proxy) for audience-bound tokens."
|
||||
)
|
||||
token_verifier = JWTVerifier(
|
||||
jwks_uri=f"{self.authkit_domain}/oauth2/jwks",
|
||||
issuer=self.authkit_domain,
|
||||
algorithm="RS256",
|
||||
audience=client_id,
|
||||
required_scopes=parsed_scopes,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class TestScalekitProvider:
|
|||
assert isinstance(provider.token_verifier, JWTVerifier)
|
||||
assert provider.token_verifier.jwks_uri == "https://my-env.scalekit.com/keys"
|
||||
assert provider.token_verifier.issuer == "https://my-env.scalekit.com"
|
||||
assert provider.token_verifier.audience is None
|
||||
assert provider.token_verifier.audience == "sk_resource_456"
|
||||
|
||||
def test_required_scopes_hooks_into_verifier(self):
|
||||
"""Token verifier should enforce required scopes when provided."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue