Add resource_server_url parameter to OAuth proxy providers (#1682)

This commit is contained in:
Jeremiah Lowin 2025-08-30 08:33:13 -04:00 committed by GitHub
commit 8c678b552b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 69 additions and 49 deletions

View file

@ -159,7 +159,7 @@ The `OAuthProxy` class provides the complete proxy implementation:
</ParamField>
<ParamField body="resource_server_url" type="AnyHttpUrl | str | None">
Resource server URL (defaults to base_url)
Path of the FastMCP server (defaults to base_url). **Important**: This should point to your MCP endpoint path. For example, if your MCP server is accessible at `{base_url}/mcp`, specify `https://your-server.com/mcp` here for proper RFC 8707 compliance.
</ParamField>
<ParamField body="allowed_client_redirect_uris" type="list[str] | None">
@ -228,7 +228,10 @@ auth = OAuthProxy(
base_url="https://your-server.com",
# Optional: customize callback path (defaults to "/auth/callback")
redirect_path="/auth/callback"
redirect_path="/auth/callback",
# Optional: specify MCP endpoint path if different from base_url
# resource_server_url="https://your-server.com/mcp"
)
mcp = FastMCP(name="My Server", auth=auth)

View file

@ -111,7 +111,7 @@ token_verifier = JWTVerifier(
auth = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
resource_server_url="https://api.yourcompany.com",
resource_server_url="https://api.yourcompany.com/mcp", # Point to your MCP endpoint
# Optional: customize allowed client redirect URIs (defaults to localhost only)
allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
)
@ -121,7 +121,7 @@ mcp = FastMCP(name="Company API", auth=auth)
This configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints.
The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation.
The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `resource_server_url` should point to your actual MCP endpoint - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use that full path rather than just the base URL.
### Custom Endpoints
@ -143,7 +143,7 @@ class CompanyAuthProvider(RemoteAuthProvider):
super().__init__(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
resource_server_url="https://api.yourcompany.com"
resource_server_url="https://api.yourcompany.com/mcp" # Your MCP endpoint path
)
def get_routes(self) -> list[Route]:

View file

@ -23,6 +23,7 @@ auth = AzureProvider(
tenant_id=os.getenv("AZURE_TENANT_ID")
or "", # Required for single-tenant apps - get from Azure Portal
base_url="http://localhost:8000",
resource_server_url="http://localhost:8000/mcp",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)

View file

@ -19,6 +19,7 @@ auth = GitHubProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
base_url="http://localhost:8000",
resource_server_url="http://localhost:8000/mcp",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)

View file

@ -19,6 +19,7 @@ auth = GoogleProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
base_url="http://localhost:8000",
resource_server_url="http://localhost:8000/mcp",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
# Optional: specify required scopes
# required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],

View file

@ -21,6 +21,7 @@ auth = WorkOSProvider(
client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "",
authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app",
base_url="http://localhost:8000",
resource_server_url="http://localhost:8000/mcp",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)

View file

@ -254,45 +254,20 @@ class OAuthProxy(OAuthProvider):
upstream_client_secret: Client secret for upstream server
upstream_revocation_endpoint: Optional upstream revocation endpoint
token_verifier: Token verifier for validating access tokens
base_url: Public URL of this FastMCP server
base_url: Public URL of the server that exposes this FastMCP server; redirect path is
relative to this URL
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
issuer_url: Issuer URL for OAuth metadata (defaults to base_url)
service_documentation_url: Optional service documentation URL
resource_server_url: Resource server URL (defaults to base_url)
resource_server_url: Path of the FastMCP server. If None, FastMCP will
attempt to overwrite this with the correct path to the server
e.g. {base_url}/mcp
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
If None (default), only localhost redirect URIs are allowed.
If empty list, all redirect URIs are allowed (not recommended for production).
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
"""
# Convert string URLs to AnyHttpUrl for parent class
base_url_parsed = (
AnyHttpUrl(base_url) if isinstance(base_url, str) else base_url
)
issuer_url_parsed = (
(AnyHttpUrl(issuer_url) if isinstance(issuer_url, str) else issuer_url)
if issuer_url
else None
)
service_documentation_url_parsed = (
(
AnyHttpUrl(service_documentation_url)
if isinstance(service_documentation_url, str)
else service_documentation_url
)
if service_documentation_url
else None
)
resource_server_url_parsed = (
(
AnyHttpUrl(resource_server_url)
if isinstance(resource_server_url, str)
else resource_server_url
)
if resource_server_url
else None
)
# Always enable DCR since we implement it locally for MCP clients
client_registration_options = ClientRegistrationOptions(enabled=True)
@ -302,13 +277,13 @@ class OAuthProxy(OAuthProvider):
)
super().__init__(
base_url=base_url_parsed,
issuer_url=issuer_url_parsed,
service_documentation_url=service_documentation_url_parsed,
base_url=base_url,
issuer_url=issuer_url,
service_documentation_url=service_documentation_url,
client_registration_options=client_registration_options,
revocation_options=revocation_options,
required_scopes=token_verifier.required_scopes,
resource_server_url=resource_server_url_parsed,
resource_server_url=resource_server_url,
)
# Store upstream configuration

View file

@ -36,6 +36,8 @@ class AzureProviderSettings(BaseSettings):
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
resource_server_url: str | None = None
allowed_client_redirect_uris: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
@ -160,7 +162,8 @@ class AzureProvider(OAuthProxy):
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | None = None,
resource_server_url: str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
):
"""Initialize Azure OAuth provider.
@ -172,6 +175,8 @@ class AzureProvider(OAuthProxy):
redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
timeout_seconds: HTTP request timeout for Azure API calls
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
"""
@ -186,6 +191,8 @@ class AzureProvider(OAuthProxy):
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"resource_server_url": resource_server_url,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
}.items()
if v is not NotSet
}
@ -220,6 +227,8 @@ class AzureProvider(OAuthProxy):
"openid",
"profile",
]
resource_server_url_final = settings.resource_server_url or base_url_final
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Extract secret string from SecretStr
client_secret_str = (
@ -250,7 +259,8 @@ class AzureProvider(OAuthProxy):
base_url=base_url_final,
redirect_path=redirect_path_final,
issuer_url=base_url_final,
allowed_client_redirect_uris=allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
resource_server_url=resource_server_url_final,
)
logger.info(

View file

@ -51,6 +51,8 @@ class GitHubProviderSettings(BaseSettings):
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
resource_server_url: AnyHttpUrl | str | None = None
allowed_client_redirect_uris: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
@ -199,9 +201,10 @@ class GitHubProvider(OAuthProxy):
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | None = None,
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
):
"""Initialize GitHub OAuth provider.
@ -212,6 +215,8 @@ class GitHubProvider(OAuthProxy):
redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
required_scopes: Required GitHub scopes (defaults to ["user"])
timeout_seconds: HTTP request timeout for GitHub API calls
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
"""
@ -225,6 +230,8 @@ class GitHubProvider(OAuthProxy):
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"resource_server_url": resource_server_url,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
}.items()
if v is not NotSet
}
@ -245,6 +252,8 @@ class GitHubProvider(OAuthProxy):
redirect_path_final = settings.redirect_path or "/auth/callback"
timeout_seconds_final = settings.timeout_seconds or 10
required_scopes_final = settings.required_scopes or ["user"]
resource_server_url_final = settings.resource_server_url or base_url_final
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Create GitHub token verifier
token_verifier = GitHubTokenVerifier(
@ -267,7 +276,8 @@ class GitHubProvider(OAuthProxy):
base_url=base_url_final,
redirect_path=redirect_path_final,
issuer_url=base_url_final, # We act as the issuer for client registration
allowed_client_redirect_uris=allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
resource_server_url=resource_server_url_final,
)
logger.info(

View file

@ -53,6 +53,8 @@ class GoogleProviderSettings(BaseSettings):
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
resource_server_url: AnyHttpUrl | str | None = None
allowed_client_redirect_uris: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
@ -215,9 +217,10 @@ class GoogleProvider(OAuthProxy):
client_secret: str | NotSetT = NotSet,
base_url: AnyHttpUrl | str | NotSetT = NotSet,
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
required_scopes: list[str] | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | None = None,
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
):
"""Initialize Google OAuth provider.
@ -244,6 +247,8 @@ class GoogleProvider(OAuthProxy):
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"resource_server_url": resource_server_url,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
}.items()
if v is not NotSet
}
@ -265,6 +270,8 @@ class GoogleProvider(OAuthProxy):
timeout_seconds_final = settings.timeout_seconds or 10
# Google requires at least one scope - openid is the minimal OIDC scope
required_scopes_final = settings.required_scopes or ["openid"]
resource_server_url_final = settings.resource_server_url or base_url_final
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Create Google token verifier
token_verifier = GoogleTokenVerifier(
@ -287,7 +294,8 @@ class GoogleProvider(OAuthProxy):
base_url=base_url_final,
redirect_path=redirect_path_final,
issuer_url=base_url_final, # We act as the issuer for client registration
allowed_client_redirect_uris=allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
resource_server_url=resource_server_url_final,
)
logger.info(

View file

@ -43,6 +43,8 @@ class WorkOSProviderSettings(BaseSettings):
redirect_path: str | None = None
required_scopes: list[str] | None = None
timeout_seconds: int | None = None
resource_server_url: AnyHttpUrl | str | None = None
allowed_client_redirect_uris: list[str] | None = None
@field_validator("required_scopes", mode="before")
@classmethod
@ -167,7 +169,8 @@ class WorkOSProvider(OAuthProxy):
redirect_path: str | NotSetT = NotSet,
required_scopes: list[str] | None | NotSetT = NotSet,
timeout_seconds: int | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | None = None,
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
):
"""Initialize WorkOS OAuth provider.
@ -179,6 +182,8 @@ class WorkOSProvider(OAuthProxy):
redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
required_scopes: Required OAuth scopes (no default)
timeout_seconds: HTTP request timeout for WorkOS API calls
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
"""
@ -193,6 +198,8 @@ class WorkOSProvider(OAuthProxy):
"redirect_path": redirect_path,
"required_scopes": required_scopes,
"timeout_seconds": timeout_seconds,
"resource_server_url": resource_server_url,
"allowed_client_redirect_uris": allowed_client_redirect_uris,
}.items()
if v is not NotSet
}
@ -221,6 +228,8 @@ class WorkOSProvider(OAuthProxy):
redirect_path_final = settings.redirect_path or "/auth/callback"
timeout_seconds_final = settings.timeout_seconds or 10
scopes_final = settings.required_scopes or []
resource_server_url_final = settings.resource_server_url or base_url_final
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
# Extract secret string from SecretStr
client_secret_str = (
@ -244,7 +253,8 @@ class WorkOSProvider(OAuthProxy):
base_url=base_url_final,
redirect_path=redirect_path_final,
issuer_url=base_url_final,
allowed_client_redirect_uris=allowed_client_redirect_uris,
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
resource_server_url=resource_server_url_final,
)
logger.info(