From fb11282e9b62e87c2a5af20e91e62ee5bf20d7b8 Mon Sep 17 00:00:00 2001 From: Eloi Zalczer Date: Fri, 26 Dec 2025 22:14:56 +0100 Subject: [PATCH] Feature/supabase custom auth route (#2632) Co-authored-by: Eloi Zalczer Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> --- docs/integrations/supabase.mdx | 14 +++++------ src/fastmcp/server/auth/providers/supabase.py | 17 ++++++++----- tests/server/auth/providers/test_supabase.py | 24 ++++++++++++++++++- 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/docs/integrations/supabase.mdx b/docs/integrations/supabase.mdx index 24dbb7ede..c4cb9afd3 100644 --- a/docs/integrations/supabase.mdx +++ b/docs/integrations/supabase.mdx @@ -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 @@ -37,7 +37,9 @@ 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", + # Optional: customize auth_route for self-hosted Supabase Auth with custom routes + # auth_route="/my/auth/route" ) mcp = FastMCP("Supabase Protected Server", auth=auth) @@ -101,12 +103,10 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider # Load configuration from environment variables auth = SupabaseProvider( - project_url=os.environ.get("SUPABASE_PROJECT_URL"), - base_url=os.environ.get("BASE_URL", "https://your-server.com") + 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) - -# Authentication is automatically configured from environment -mcp = FastMCP(name="Supabase Protected Server") ``` diff --git a/src/fastmcp/server/auth/providers/supabase.py b/src/fastmcp/server/auth/providers/supabase.py index ffc68d1cc..513c02555 100644 --- a/src/fastmcp/server/auth/providers/supabase.py +++ b/src/fastmcp/server/auth/providers/supabase.py @@ -38,8 +38,9 @@ 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} + - Default auth_route is "/auth/v1" (can be customized for self-hosted setups) - Tokens are cached for up to 10 minutes by Supabase's edge servers - Algorithm must match your Supabase Auth configuration @@ -72,6 +73,7 @@ class SupabaseProvider(RemoteAuthProvider): *, project_url: AnyHttpUrl | str, base_url: AnyHttpUrl | str, + auth_route: str = "/auth/v1", algorithm: Literal["HS256", "RS256", "ES256"] = "ES256", required_scopes: list[str] | None = None, token_verifier: TokenVerifier | None = None, @@ -81,6 +83,8 @@ class SupabaseProvider(RemoteAuthProvider): 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". Can be customized + for self-hosted Supabase Auth setups using custom routes. 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. @@ -90,6 +94,7 @@ class SupabaseProvider(RemoteAuthProvider): """ self.project_url = str(project_url).rstrip("/") self.base_url = AnyHttpUrl(str(base_url).rstrip("/")) + self.auth_route = auth_route.strip("/") # Parse scopes if provided as string parsed_scopes = ( @@ -99,8 +104,8 @@ class SupabaseProvider(RemoteAuthProvider): # 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=algorithm, required_scopes=parsed_scopes, ) @@ -108,7 +113,7 @@ class SupabaseProvider(RemoteAuthProvider): # 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, ) @@ -133,7 +138,7 @@ class SupabaseProvider(RemoteAuthProvider): 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() diff --git a/tests/server/auth/providers/test_supabase.py b/tests/server/auth/providers/test_supabase.py index 1abe4e9a0..70c21085f 100644 --- a/tests/server/auth/providers/test_supabase.py +++ b/tests/server/auth/providers/test_supabase.py @@ -58,7 +58,7 @@ class TestSupabaseProvider: base_url="https://myserver.com", ) - # Check that JWT verifier uses the correct endpoints + # Check that JWT verifier uses the correct endpoints (default auth_route) assert isinstance(provider.token_verifier, JWTVerifier) assert ( provider.token_verifier.jwks_uri @@ -127,6 +127,28 @@ class TestSupabaseProvider: assert isinstance(provider.token_verifier, JWTVerifier) assert provider.token_verifier.algorithm == "RS256" + 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(