Skip to content

Commit d480587

Browse files
authored
Merge pull request #99 from golf-mcp/asch/fix-0.2.0-oauth
Asch/fix 0.2.0 oauth
2 parents ede9296 + 8079fe6 commit d480587

File tree

10 files changed

+1151
-94
lines changed

10 files changed

+1151
-94
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Makefile
1212
htmlcov/
1313
.ruff_cache/
1414
new/
15+
.DS_Store
1516

1617
# Generated endpoints file (created by editable install)
1718
src/golf/_endpoints.py

setup.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,39 @@ def render_endpoints(require_env_vars: bool = True):
1919

2020
# Get environment variables
2121
platform_url = os.environ.get("GOLF_PLATFORM_API_URL")
22-
otel_url = os.environ.get("GOLF_OTEL_ENDPOINT")
23-
22+
otel_url = os.environ.get("GOLF_OTEL_ENDPOINT")
23+
2424
# For production builds, require environment variables
2525
# For development/editable installs, use fallback values
2626
if require_env_vars and (not platform_url or not otel_url):
2727
raise SystemExit(
2828
"Missing required environment variables for URL injection:\n"
29-
" GOLF_PLATFORM_API_URL\n"
29+
" GOLF_PLATFORM_API_URL\n"
3030
" GOLF_OTEL_ENDPOINT\n"
3131
"Set these before building the package."
3232
)
33-
33+
3434
# Use environment variables if available, otherwise fallback to development URLs
3535
values = {
3636
"PLATFORM_API_URL": platform_url or "http://localhost:8000/api/resources",
3737
"OTEL_ENDPOINT": otel_url or "http://localhost:4318/v1/traces",
3838
}
39-
39+
4040
try:
4141
rendered = tpl_path.read_text(encoding="utf-8").format(**values)
4242
except KeyError as e:
4343
raise SystemExit(f"Missing template key: {e}") from e
44-
44+
4545
return rendered
4646

4747

4848
class build_py(_build_py):
4949
"""Custom build_py that renders endpoints into the build_lib (wheel contents)."""
50-
50+
5151
def run(self):
5252
# First run the normal build
5353
super().run()
54-
54+
5555
# Then render endpoints into the build_lib
5656
# Skip env var requirement if in CI environment or if this looks like a test install
5757
is_ci = os.environ.get("CI") or os.environ.get("GITHUB_ACTIONS")
@@ -64,11 +64,11 @@ def run(self):
6464

6565
class develop(_develop):
6666
"""Custom develop for editable installs - generates endpoints file in source tree."""
67-
67+
6868
def run(self):
6969
# Run normal develop command first
7070
super().run()
71-
71+
7272
# Generate a working copy file for editable installs (use fallback URLs if env vars missing)
7373
rendered = render_endpoints(require_env_vars=False)
7474
# For editable installs, write into the source tree so imports work
@@ -84,4 +84,4 @@ def run(self):
8484
"build_py": build_py,
8585
"develop": develop,
8686
}
87-
)
87+
)

src/golf/auth/__init__.py

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
create_simple_jwt_provider,
2121
create_dev_token_provider,
2222
)
23+
from .registry import (
24+
BaseProviderPlugin,
25+
AuthProviderFactory,
26+
get_provider_registry,
27+
register_provider_factory,
28+
register_provider_plugin,
29+
)
2330

2431
# Re-export for backward compatibility
2532
from .api_key import configure_api_key, get_api_key_config, is_api_key_configured
@@ -48,6 +55,12 @@
4855
"create_auth_provider",
4956
"create_simple_jwt_provider",
5057
"create_dev_token_provider",
58+
# Provider registry and plugins
59+
"BaseProviderPlugin",
60+
"AuthProviderFactory",
61+
"get_provider_registry",
62+
"register_provider_factory",
63+
"register_provider_plugin",
5164
# API key functions (backward compatibility)
5265
"configure_api_key",
5366
"get_api_key_config",
@@ -61,18 +74,18 @@
6174
]
6275

6376
# Global storage for auth configuration
64-
_auth_config: tuple[AuthConfig, list[str] | None] | None = None
77+
_auth_config: AuthConfig | None = None
6578

6679

67-
def configure_auth(config: AuthConfig, required_scopes: list[str] | None = None) -> None:
80+
def configure_auth(config: AuthConfig) -> None:
6881
"""Configure authentication for the Golf server.
6982
7083
This function should be called in auth.py to set up authentication
7184
using FastMCP's modern auth providers.
7285
7386
Args:
7487
config: Authentication configuration (JWT, OAuth, Static, or Remote)
75-
required_scopes: Optional list of scopes required for all requests
88+
The required_scopes should be specified in the config itself.
7689
7790
Examples:
7891
# JWT authentication with Auth0
@@ -97,7 +110,8 @@ def configure_auth(config: AuthConfig, required_scopes: list[str] | None = None)
97110
"client_id": "dev-client",
98111
"scopes": ["read", "write"],
99112
}
100-
}
113+
},
114+
required_scopes=["read"],
101115
)
102116
)
103117
@@ -109,11 +123,12 @@ def configure_auth(config: AuthConfig, required_scopes: list[str] | None = None)
109123
base_url="https://your-server.example.com",
110124
valid_scopes=["read", "write", "admin"],
111125
default_scopes=["read"],
126+
required_scopes=["read"],
112127
)
113128
)
114129
"""
115130
global _auth_config
116-
_auth_config = (config, required_scopes)
131+
_auth_config = config
117132

118133

119134
def configure_jwt_auth(
@@ -144,7 +159,7 @@ def configure_jwt_auth(
144159
required_scopes=required_scopes or [],
145160
**env_vars,
146161
)
147-
configure_auth(config, required_scopes)
162+
configure_auth(config)
148163

149164

150165
def configure_dev_auth(
@@ -173,14 +188,14 @@ def configure_dev_auth(
173188
tokens=tokens,
174189
required_scopes=required_scopes or [],
175190
)
176-
configure_auth(config, required_scopes)
191+
configure_auth(config)
177192

178193

179-
def get_auth_config() -> tuple[AuthConfig, list[str] | None] | None:
194+
def get_auth_config() -> AuthConfig | None:
180195
"""Get the current auth configuration.
181196
182197
Returns:
183-
Tuple of (auth_config, required_scopes) if configured, None otherwise
198+
AuthConfig if configured, None otherwise
184199
"""
185200
return _auth_config
186201

@@ -204,9 +219,8 @@ def create_auth_provider_from_config() -> object | None:
204219
Returns:
205220
FastMCP AuthProvider instance or None if not configured
206221
"""
207-
config_tuple = get_auth_config()
208-
if not config_tuple:
222+
config = get_auth_config()
223+
if not config:
209224
return None
210225

211-
config, _ = config_tuple
212226
return create_auth_provider(config)

src/golf/auth/factory.py

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if TYPE_CHECKING:
1010
from fastmcp.server.auth.auth import AuthProvider
1111
from fastmcp.server.auth import JWTVerifier, StaticTokenVerifier
12-
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
12+
from mcp.server.auth.settings import RevocationOptions
1313

1414
from .providers import (
1515
AuthConfig,
@@ -18,11 +18,19 @@
1818
OAuthServerConfig,
1919
RemoteAuthConfig,
2020
)
21+
from .registry import (
22+
get_provider_registry,
23+
create_auth_provider_from_registry,
24+
)
2125

2226

2327
def create_auth_provider(config: AuthConfig) -> "AuthProvider":
2428
"""Create a FastMCP AuthProvider from Golf auth configuration.
2529
30+
This function uses the provider registry system to allow extensibility.
31+
Built-in providers are automatically registered, and custom providers
32+
can be added via the registry system.
33+
2634
Args:
2735
config: Golf authentication configuration
2836
@@ -32,17 +40,23 @@ def create_auth_provider(config: AuthConfig) -> "AuthProvider":
3240
Raises:
3341
ValueError: If configuration is invalid
3442
ImportError: If required dependencies are missing
43+
KeyError: If provider type is not registered
3544
"""
36-
if config.provider_type == "jwt":
37-
return _create_jwt_provider(config)
38-
elif config.provider_type == "static":
39-
return _create_static_provider(config)
40-
elif config.provider_type == "oauth_server":
41-
return _create_oauth_server_provider(config)
42-
elif config.provider_type == "remote":
43-
return _create_remote_provider(config)
44-
else:
45-
raise ValueError(f"Unknown provider type: {config.provider_type}")
45+
try:
46+
return create_auth_provider_from_registry(config)
47+
except KeyError:
48+
# Fall back to legacy dispatch for backward compatibility
49+
# This ensures existing code continues to work during transition
50+
if config.provider_type == "jwt":
51+
return _create_jwt_provider(config)
52+
elif config.provider_type == "static":
53+
return _create_static_provider(config)
54+
elif config.provider_type == "oauth_server":
55+
return _create_oauth_server_provider(config)
56+
elif config.provider_type == "remote":
57+
return _create_remote_provider(config)
58+
else:
59+
raise ValueError(f"Unknown provider type: {config.provider_type}") from None
4660

4761

4862
def _create_jwt_provider(config: JWTAuthConfig) -> "JWTVerifier":
@@ -123,21 +137,61 @@ def _create_oauth_server_provider(config: OAuthServerConfig) -> "AuthProvider":
123137
"OAuthProvider not available in this FastMCP version. Please upgrade to FastMCP 2.11.0 or later."
124138
) from e
125139

126-
# Resolve runtime values from environment variables
140+
# Resolve runtime values from environment variables with validation
127141
base_url = config.base_url
128142
if config.base_url_env_var:
129143
env_value = os.environ.get(config.base_url_env_var)
130144
if env_value:
131-
base_url = env_value
145+
# Apply the same validation as the config field to env var value
146+
try:
147+
from urllib.parse import urlparse
148+
149+
env_value = env_value.strip()
150+
parsed = urlparse(env_value)
151+
152+
if not parsed.scheme or not parsed.netloc:
153+
raise ValueError(
154+
f"Invalid base URL from environment variable {config.base_url_env_var}: '{env_value}'"
155+
)
156+
157+
if parsed.scheme not in ("http", "https"):
158+
raise ValueError(f"Base URL from environment must use http/https: '{env_value}'")
159+
160+
# Production HTTPS check
161+
is_production = (
162+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
163+
or os.environ.get("NODE_ENV", "").lower() == "production"
164+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
165+
)
166+
167+
if is_production and parsed.scheme == "http":
168+
raise ValueError(f"Base URL must use HTTPS in production: '{env_value}'")
169+
170+
base_url = env_value
171+
172+
except Exception as e:
173+
raise ValueError(f"Invalid base URL from environment variable {config.base_url_env_var}: {e}") from e
174+
175+
# Additional security validations before creating provider
176+
from urllib.parse import urlparse
177+
178+
# Validate final base_url
179+
parsed_base = urlparse(base_url)
180+
if not parsed_base.scheme or not parsed_base.netloc:
181+
raise ValueError(f"Invalid base URL: '{base_url}'")
182+
183+
# Security check: prevent localhost in production
184+
is_production = (
185+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
186+
or os.environ.get("NODE_ENV", "").lower() == "production"
187+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
188+
)
189+
190+
if is_production and parsed_base.hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
191+
raise ValueError(f"Cannot use localhost/loopback addresses in production: '{base_url}'")
132192

133-
# Create client registration options
193+
# Client registration options - always disabled for security
134194
client_reg_options = None
135-
if config.allow_client_registration:
136-
client_reg_options = ClientRegistrationOptions(
137-
enabled=True,
138-
valid_scopes=config.valid_scopes,
139-
default_scopes=config.default_scopes,
140-
)
141195

142196
# Create revocation options
143197
revocation_options = None
@@ -163,6 +217,20 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
163217
"RemoteAuthProvider not available in this FastMCP version. Please upgrade to FastMCP 2.11.0 or later."
164218
) from e
165219

220+
# Resolve runtime values from environment variables
221+
authorization_servers = config.authorization_servers
222+
if config.authorization_servers_env_var:
223+
env_value = os.environ.get(config.authorization_servers_env_var)
224+
if env_value:
225+
# Split comma-separated values and strip whitespace
226+
authorization_servers = [s.strip() for s in env_value.split(",")]
227+
228+
resource_server_url = config.resource_server_url
229+
if config.resource_server_url_env_var:
230+
env_value = os.environ.get(config.resource_server_url_env_var)
231+
if env_value:
232+
resource_server_url = env_value
233+
166234
# Create the underlying token verifier
167235
token_verifier = create_auth_provider(config.token_verifier_config)
168236

@@ -172,8 +240,8 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
172240

173241
return RemoteAuthProvider(
174242
token_verifier=token_verifier,
175-
authorization_servers=config.authorization_servers,
176-
resource_server_url=config.resource_server_url,
243+
authorization_servers=authorization_servers,
244+
resource_server_url=resource_server_url,
177245
)
178246

179247

@@ -241,3 +309,25 @@ def create_dev_token_provider(
241309
required_scopes=required_scopes or [],
242310
)
243311
return _create_static_provider(config)
312+
313+
314+
def register_builtin_providers() -> None:
315+
"""Register built-in authentication providers in the registry.
316+
317+
This function registers the standard Golf authentication providers:
318+
- jwt: JWT token verification
319+
- static: Static token verification (development)
320+
- oauth_server: Full OAuth authorization server
321+
- remote: Remote authorization server integration
322+
"""
323+
registry = get_provider_registry()
324+
325+
# Register built-in provider factories
326+
registry.register_factory("jwt", _create_jwt_provider)
327+
registry.register_factory("static", _create_static_provider)
328+
registry.register_factory("oauth_server", _create_oauth_server_provider)
329+
registry.register_factory("remote", _create_remote_provider)
330+
331+
332+
# Register built-in providers when module is imported
333+
register_builtin_providers()

0 commit comments

Comments
 (0)