Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions docs/integrations/supabase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This guide shows you how to secure your FastMCP server using **Supabase Auth**.
### Prerequisites

Before you begin, you will need:
1. A **[Supabase Account](https://supabase.com/)** with a project
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`)

### Step 1: Get Supabase Project URL
Expand All @@ -37,7 +37,8 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider
# Configure Supabase Auth
auth = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="http://localhost:8000"
base_url="http://localhost:8000",
auth_route="/my/auth/route" # if self-hosting and using custom routes
)

mcp = FastMCP("Supabase Protected Server", auth=auth)
Expand Down Expand Up @@ -117,6 +118,10 @@ Your Supabase project URL (e.g., `https://abc123.supabase.co`)
Public URL of your FastMCP server (e.g., `https://your-server.com` or `http://localhost:8000` for development)
</ParamField>

<ParamField path="FASTMCP_SERVER_AUTH_SUPABASE_AUTH_ROUTE" default="/auth/v1">
Your Supabase auth route (e.g., `/auth/v1`)
</ParamField>

<ParamField path="FASTMCP_SERVER_AUTH_SUPABASE_REQUIRED_SCOPES" default="[]">
Comma-, space-, or JSON-separated list of required OAuth scopes (e.g., `openid email` or `["openid", "email"]`)
</ParamField>
Expand Down
17 changes: 11 additions & 6 deletions src/fastmcp/server/auth/providers/supabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class SupabaseProviderSettings(BaseSettings):

project_url: AnyHttpUrl
base_url: AnyHttpUrl
auth_route: str = "/auth/v1"
algorithm: Literal["HS256", "RS256", "ES256"] = "ES256"
required_scopes: list[str] | None = None

Expand All @@ -59,8 +60,8 @@ class SupabaseProvider(RemoteAuthProvider):
- Asymmetric keys (RS256/ES256) are recommended for production

2. JWT Verification:
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}/auth/v1/.well-known/jwks.json
- JWTs are issued by {project_url}/auth/v1
- FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
- JWTs are issued by {project_url}{auth_route}
- Tokens are cached for up to 10 minutes by Supabase's edge servers
- Algorithm must match your Supabase Auth configuration

Expand Down Expand Up @@ -93,6 +94,7 @@ def __init__(
*,
project_url: AnyHttpUrl | str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
auth_route: str | NotSetT = NotSet,
algorithm: Literal["HS256", "RS256", "ES256"] | NotSetT = NotSet,
required_scopes: list[str] | NotSetT | None = NotSet,
token_verifier: TokenVerifier | None = None,
Expand All @@ -102,6 +104,7 @@ def __init__(
Args:
project_url: Your Supabase project URL (e.g., "https://abc123.supabase.co")
base_url: Public URL of this FastMCP server
auth_route: Supabase Auth route. Defaults to "/auth/v1".
algorithm: JWT signing algorithm (HS256, RS256, or ES256). Must match your
Supabase Auth configuration. Defaults to ES256.
required_scopes: Optional list of scopes to require for all requests.
Expand All @@ -115,6 +118,7 @@ def __init__(
for k, v in {
"project_url": project_url,
"base_url": base_url,
"auth_route": auth_route,
"algorithm": algorithm,
"required_scopes": required_scopes,
}.items()
Expand All @@ -124,20 +128,21 @@ def __init__(

self.project_url = str(settings.project_url).rstrip("/")
self.base_url = AnyHttpUrl(str(settings.base_url).rstrip("/"))
self.auth_route = settings.auth_route.strip("/")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add validation to prevent empty auth_route after stripping.

If a user passes auth_route="/", the strip("/") operation results in an empty string, which would produce malformed URLs like "https://example.com//.well-known/jwks.json" (double slashes).

🔎 Suggested fix to validate auth_route
 self.auth_route = settings.auth_route.strip("/")
+if not self.auth_route:
+    raise ValueError("auth_route cannot be empty after removing slashes")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self.auth_route = settings.auth_route.strip("/")
self.auth_route = settings.auth_route.strip("/")
if not self.auth_route:
raise ValueError("auth_route cannot be empty after removing slashes")


# Create default JWT verifier if none provided
if token_verifier is None:
token_verifier = JWTVerifier(
jwks_uri=f"{self.project_url}/auth/v1/.well-known/jwks.json",
issuer=f"{self.project_url}/auth/v1",
jwks_uri=f"{self.project_url}/{self.auth_route}/.well-known/jwks.json",
issuer=f"{self.project_url}/{self.auth_route}",
algorithm=settings.algorithm,
required_scopes=settings.required_scopes,
)

# Initialize RemoteAuthProvider with Supabase as the authorization server
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl(f"{self.project_url}/auth/v1")],
authorization_servers=[AnyHttpUrl(f"{self.project_url}/{self.auth_route}")],
base_url=self.base_url,
)

Expand All @@ -162,7 +167,7 @@ async def oauth_authorization_server_metadata(request):
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.project_url}/auth/v1/.well-known/oauth-authorization-server"
f"{self.project_url}/{self.auth_route}/.well-known/oauth-authorization-server"
)
response.raise_for_status()
metadata = response.json()
Expand Down
24 changes: 24 additions & 0 deletions tests/server/auth/providers/test_supabase.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ def test_init_with_env_vars(self, scopes_env):
{
"FASTMCP_SERVER_AUTH_SUPABASE_PROJECT_URL": "https://env123.supabase.co",
"FASTMCP_SERVER_AUTH_SUPABASE_BASE_URL": "https://envserver.com",
"FASTMCP_SERVER_AUTH_SUPABASE_AUTH_ROUTE": "/custom/auth/route",
},
):
provider = SupabaseProvider()

assert provider.project_url == "https://env123.supabase.co"
assert str(provider.base_url) == "https://envserver.com/"
assert provider.auth_route == "custom/auth/route"

def test_environment_variable_loading(self):
"""Test that environment variables are loaded correctly."""
Expand Down Expand Up @@ -150,6 +152,28 @@ def test_algorithm_from_env_var(self):

assert provider.token_verifier.algorithm == "RS256" # type: ignore[attr-defined]

def test_custom_auth_route(self):
provider = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="https://myserver.com",
auth_route="/custom/auth/route",
)

assert provider.auth_route == "custom/auth/route"
assert (
provider.token_verifier.jwks_uri
== "https://abc123.supabase.co/custom/auth/route/.well-known/jwks.json"
) # type: ignore[attr-defined]

def test_custom_auth_route_trailing_slash(self):
provider = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="https://myserver.com",
auth_route="/custom/auth/route/",
)

assert provider.auth_route == "custom/auth/route"


def run_mcp_server(host: str, port: int) -> None:
mcp = FastMCP(
Expand Down
Loading