Skip to content

Commit f4d9d74

Browse files
authored
Merge pull request #125 from golf-mcp/aschlean/fix-stubs-in-config
fix: not require stubs when env vars set
2 parents 642ec6b + b757b80 commit f4d9d74

File tree

6 files changed

+325
-52
lines changed

6 files changed

+325
-52
lines changed

.github/SECURITY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## Supported Versions
44
| Version | Supported |
55
| ------- | ------------------ |
6+
| 0.2.18 | :white_check_mark: |
67
| 0.2.17 | :white_check_mark: |
78
| 0.2.16 | :white_check_mark: |
89
| 0.2.15 | :white_check_mark: |

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "golf-mcp"
7-
version = "0.2.17"
7+
version = "0.2.18"
88
description = "Framework for building MCP servers"
99
authors = [
1010
{name = "Antoni Gmitruk", email = "antoni@golf.dev"}
@@ -66,7 +66,7 @@ golf = ["examples/**/*"]
6666

6767
[tool.poetry]
6868
name = "golf-mcp"
69-
version = "0.2.17"
69+
version = "0.2.18"
7070
description = "Framework for building MCP servers with zero boilerplate"
7171
authors = ["Antoni Gmitruk <antoni@golf.dev>"]
7272
license = "Apache-2.0"

src/golf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.2.17"
1+
__version__ = "0.2.18"

src/golf/auth/__init__.py

Lines changed: 67 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -193,40 +193,84 @@ def configure_dev_auth(
193193

194194

195195
def configure_oauth_proxy(
196-
authorization_endpoint: str,
197-
token_endpoint: str,
198-
client_id: str,
199-
client_secret: str,
200-
base_url: str,
201-
token_verifier_config: JWTAuthConfig | StaticTokenConfig,
196+
authorization_endpoint: str | None = None,
197+
token_endpoint: str | None = None,
198+
client_id: str | None = None,
199+
client_secret: str | None = None,
200+
base_url: str | None = None,
201+
token_verifier_config: JWTAuthConfig | StaticTokenConfig | None = None,
202202
scopes_supported: list[str] | None = None,
203203
revocation_endpoint: str | None = None,
204204
redirect_path: str = "/oauth/callback",
205205
**env_vars: str,
206206
) -> None:
207207
"""Configure OAuth proxy authentication for non-DCR providers.
208208
209-
This sets up an OAuth proxy that bridges MCP clients (expecting DCR) with
210-
traditional OAuth providers like GitHub, Google, Okta Web Apps that use
211-
fixed client credentials.
209+
All parameters can be provided either directly or via environment variables.
210+
For each parameter, you can provide the value directly or use the
211+
corresponding *_env_var parameter to specify an environment variable name.
212+
213+
Examples:
214+
# Direct values (backward compatible)
215+
configure_oauth_proxy(
216+
authorization_endpoint="https://auth.example.com/authorize",
217+
token_endpoint="https://auth.example.com/token",
218+
client_id="my-client",
219+
client_secret="my-secret",
220+
base_url="https://myserver.com",
221+
token_verifier_config=jwt_config,
222+
)
223+
224+
# Environment variables only (new behavior)
225+
configure_oauth_proxy(
226+
authorization_endpoint_env_var="OAUTH_AUTH_ENDPOINT",
227+
token_endpoint_env_var="OAUTH_TOKEN_ENDPOINT",
228+
client_id_env_var="OAUTH_CLIENT_ID",
229+
client_secret_env_var="OAUTH_CLIENT_SECRET",
230+
base_url_env_var="OAUTH_BASE_URL",
231+
token_verifier_config=jwt_config,
232+
)
233+
234+
# Mixed (direct values with env var overrides)
235+
configure_oauth_proxy(
236+
authorization_endpoint="https://default.example.com/authorize",
237+
authorization_endpoint_env_var="OAUTH_AUTH_ENDPOINT", # Overrides at runtime
238+
# ...
239+
)
212240
213241
Args:
214-
authorization_endpoint: Provider's authorization URL
215-
token_endpoint: Provider's token endpoint URL
216-
client_id: Your client ID registered with the provider
217-
client_secret: Your client secret from the provider
218-
base_url: This proxy server's public URL
219-
token_verifier_config: JWT or static token config for token verification
220-
scopes_supported: Scopes to advertise to MCP clients
242+
authorization_endpoint: OAuth provider's authorization endpoint URL
243+
token_endpoint: OAuth provider's token endpoint URL
244+
client_id: Your registered client ID with the OAuth provider
245+
client_secret: Your registered client secret with the OAuth provider
246+
base_url: Public URL of this OAuth proxy server
247+
token_verifier_config: JWT or Static token configuration for verifying tokens
248+
scopes_supported: List of OAuth scopes this proxy supports
221249
revocation_endpoint: Optional token revocation endpoint
222-
redirect_path: OAuth callback path (default: /oauth/callback)
223-
**env_vars: Environment variable names (authorization_endpoint_env_var,
224-
token_endpoint_env_var, client_id_env_var, client_secret_env_var,
225-
base_url_env_var, revocation_endpoint_env_var)
226-
227-
Note:
228-
Requires golf-mcp-enterprise package for implementation.
250+
redirect_path: OAuth callback path (default: "/oauth/callback")
251+
**env_vars: Environment variable names for runtime configuration
252+
- authorization_endpoint_env_var: Env var for authorization endpoint
253+
- token_endpoint_env_var: Env var for token endpoint
254+
- client_id_env_var: Env var for client ID
255+
- client_secret_env_var: Env var for client secret
256+
- base_url_env_var: Env var for base URL
257+
- revocation_endpoint_env_var: Env var for revocation endpoint
258+
259+
Raises:
260+
ValueError: If token_verifier_config is not provided or invalid
261+
ValueError: If required fields lack both direct value and env var
229262
"""
263+
# Validate token_verifier_config is provided (always required)
264+
if token_verifier_config is None:
265+
raise ValueError("token_verifier_config is required and must be JWTAuthConfig or StaticTokenConfig")
266+
267+
if not isinstance(token_verifier_config, (JWTAuthConfig, StaticTokenConfig)):
268+
raise ValueError(
269+
f"token_verifier_config must be JWTAuthConfig or StaticTokenConfig, "
270+
f"got {type(token_verifier_config).__name__}"
271+
)
272+
273+
# Create config with all parameters (None values are OK now)
230274
config = OAuthProxyConfig(
231275
authorization_endpoint=authorization_endpoint,
232276
token_endpoint=token_endpoint,

src/golf/auth/factory.py

Lines changed: 208 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,19 +254,218 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
254254

255255

256256
def _create_oauth_proxy_provider(config: OAuthProxyConfig) -> "AuthProvider":
257-
"""Create OAuth proxy provider - requires enterprise package."""
257+
"""Create OAuth proxy provider from configuration with runtime validation."""
258+
# Resolve runtime values from environment variables
259+
authorization_endpoint = config.authorization_endpoint
260+
if config.authorization_endpoint_env_var:
261+
env_value = os.environ.get(config.authorization_endpoint_env_var)
262+
if env_value:
263+
# Validate the URL from environment
264+
env_value = env_value.strip()
265+
try:
266+
from urllib.parse import urlparse
267+
parsed = urlparse(env_value)
268+
if not parsed.scheme or not parsed.netloc:
269+
raise ValueError(
270+
f"Invalid authorization_endpoint from environment variable "
271+
f"{config.authorization_endpoint_env_var}: '{env_value}' - "
272+
f"must be a valid URL with scheme and netloc"
273+
)
274+
if parsed.scheme not in ("http", "https"):
275+
raise ValueError(
276+
f"Authorization endpoint from {config.authorization_endpoint_env_var} "
277+
f"must use http or https: '{env_value}'"
278+
)
279+
authorization_endpoint = env_value
280+
except Exception as e:
281+
if isinstance(e, ValueError):
282+
raise
283+
raise ValueError(
284+
f"Invalid authorization_endpoint from {config.authorization_endpoint_env_var}: {e}"
285+
) from e
286+
287+
token_endpoint = config.token_endpoint
288+
if config.token_endpoint_env_var:
289+
env_value = os.environ.get(config.token_endpoint_env_var)
290+
if env_value:
291+
# Validate the URL from environment
292+
env_value = env_value.strip()
293+
try:
294+
from urllib.parse import urlparse
295+
parsed = urlparse(env_value)
296+
if not parsed.scheme or not parsed.netloc:
297+
raise ValueError(
298+
f"Invalid token_endpoint from environment variable "
299+
f"{config.token_endpoint_env_var}: '{env_value}'"
300+
)
301+
if parsed.scheme not in ("http", "https"):
302+
raise ValueError(
303+
f"Token endpoint from {config.token_endpoint_env_var} "
304+
f"must use http or https: '{env_value}'"
305+
)
306+
token_endpoint = env_value
307+
except Exception as e:
308+
if isinstance(e, ValueError):
309+
raise
310+
raise ValueError(
311+
f"Invalid token_endpoint from {config.token_endpoint_env_var}: {e}"
312+
) from e
313+
314+
client_id = config.client_id
315+
if config.client_id_env_var:
316+
env_value = os.environ.get(config.client_id_env_var)
317+
if env_value:
318+
client_id = env_value.strip()
319+
if not client_id:
320+
raise ValueError(
321+
f"Client ID from environment variable {config.client_id_env_var} cannot be empty"
322+
)
323+
324+
client_secret = config.client_secret
325+
if config.client_secret_env_var:
326+
env_value = os.environ.get(config.client_secret_env_var)
327+
if env_value:
328+
client_secret = env_value.strip()
329+
if not client_secret:
330+
raise ValueError(
331+
f"Client secret from environment variable {config.client_secret_env_var} cannot be empty"
332+
)
333+
334+
base_url = config.base_url
335+
if config.base_url_env_var:
336+
env_value = os.environ.get(config.base_url_env_var)
337+
if env_value:
338+
# Validate the URL from environment
339+
env_value = env_value.strip()
340+
try:
341+
from urllib.parse import urlparse
342+
parsed = urlparse(env_value)
343+
if not parsed.scheme or not parsed.netloc:
344+
raise ValueError(
345+
f"Invalid base_url from environment variable "
346+
f"{config.base_url_env_var}: '{env_value}'"
347+
)
348+
if parsed.scheme not in ("http", "https"):
349+
raise ValueError(
350+
f"Base URL from {config.base_url_env_var} "
351+
f"must use http or https: '{env_value}'"
352+
)
353+
base_url = env_value
354+
except Exception as e:
355+
if isinstance(e, ValueError):
356+
raise
357+
raise ValueError(
358+
f"Invalid base_url from {config.base_url_env_var}: {e}"
359+
) from e
360+
361+
revocation_endpoint = config.revocation_endpoint
362+
if config.revocation_endpoint_env_var:
363+
env_value = os.environ.get(config.revocation_endpoint_env_var)
364+
if env_value:
365+
# Validate optional URL from environment
366+
env_value = env_value.strip()
367+
if env_value: # Only validate if not empty
368+
try:
369+
from urllib.parse import urlparse
370+
parsed = urlparse(env_value)
371+
if not parsed.scheme or not parsed.netloc:
372+
raise ValueError(
373+
f"Invalid revocation_endpoint from environment variable "
374+
f"{config.revocation_endpoint_env_var}: '{env_value}'"
375+
)
376+
if parsed.scheme not in ("http", "https"):
377+
raise ValueError(
378+
f"Revocation endpoint from {config.revocation_endpoint_env_var} "
379+
f"must use http or https: '{env_value}'"
380+
)
381+
revocation_endpoint = env_value
382+
except Exception as e:
383+
if isinstance(e, ValueError):
384+
raise
385+
raise ValueError(
386+
f"Invalid revocation_endpoint from {config.revocation_endpoint_env_var}: {e}"
387+
) from e
388+
389+
# Final validation: ensure all required fields have values after env resolution
390+
if not authorization_endpoint:
391+
env_var_hint = f" (environment variable {config.authorization_endpoint_env_var} is not set)" \
392+
if config.authorization_endpoint_env_var else ""
393+
raise ValueError(f"Authorization endpoint is required but not provided{env_var_hint}")
394+
395+
if not token_endpoint:
396+
env_var_hint = f" (environment variable {config.token_endpoint_env_var} is not set)" \
397+
if config.token_endpoint_env_var else ""
398+
raise ValueError(f"Token endpoint is required but not provided{env_var_hint}")
399+
400+
if not client_id:
401+
env_var_hint = f" (environment variable {config.client_id_env_var} is not set)" \
402+
if config.client_id_env_var else ""
403+
raise ValueError(f"Client ID is required but not provided{env_var_hint}")
404+
405+
if not client_secret:
406+
env_var_hint = f" (environment variable {config.client_secret_env_var} is not set)" \
407+
if config.client_secret_env_var else ""
408+
raise ValueError(f"Client secret is required but not provided{env_var_hint}")
409+
410+
if not base_url:
411+
env_var_hint = f" (environment variable {config.base_url_env_var} is not set)" \
412+
if config.base_url_env_var else ""
413+
raise ValueError(f"Base URL is required but not provided{env_var_hint}")
414+
415+
# Production security checks
416+
is_production = (
417+
os.environ.get("GOLF_ENV", "").lower() in ("prod", "production")
418+
or os.environ.get("NODE_ENV", "").lower() == "production"
419+
or os.environ.get("ENVIRONMENT", "").lower() in ("prod", "production")
420+
)
421+
422+
if is_production:
423+
from urllib.parse import urlparse
424+
425+
# Check for HTTPS in production
426+
for url_name, url_value in [
427+
("authorization_endpoint", authorization_endpoint),
428+
("token_endpoint", token_endpoint),
429+
("base_url", base_url),
430+
]:
431+
parsed = urlparse(url_value)
432+
if parsed.scheme == "http":
433+
raise ValueError(
434+
f"OAuth proxy {url_name} must use HTTPS in production environment: '{url_value}'"
435+
)
436+
437+
# Check for localhost in production
438+
parsed_base = urlparse(base_url)
439+
if parsed_base.hostname in ("localhost", "127.0.0.1", "0.0.0.0"):
440+
raise ValueError(
441+
f"OAuth proxy base_url cannot use localhost/loopback addresses in production: '{base_url}'"
442+
)
443+
444+
# Import and create the OAuth proxy provider
258445
try:
259446
# Try to import from enterprise package
260447
from golf_enterprise import create_oauth_proxy_provider
261-
262-
return create_oauth_proxy_provider(config)
263-
except ImportError as e:
448+
except ImportError:
449+
# Provide helpful error message
264450
raise ImportError(
265-
"OAuth Proxy requires golf-mcp-enterprise package. "
266-
"This feature provides OAuth proxy functionality for non-DCR providers "
267-
"(GitHub, Google, Okta Web Apps, etc.). "
268-
"Contact sales@golf.dev for enterprise licensing."
269-
) from e
451+
"OAuth proxy authentication requires the golf-mcp-enterprise package. "
452+
"Please install it with: pip install golf-mcp-enterprise"
453+
) from None
454+
455+
# Create a new config with resolved values for the enterprise package
456+
resolved_config = OAuthProxyConfig(
457+
authorization_endpoint=authorization_endpoint,
458+
token_endpoint=token_endpoint,
459+
client_id=client_id,
460+
client_secret=client_secret,
461+
revocation_endpoint=revocation_endpoint,
462+
base_url=base_url,
463+
redirect_path=config.redirect_path,
464+
scopes_supported=config.scopes_supported,
465+
token_verifier_config=config.token_verifier_config,
466+
)
467+
468+
return create_oauth_proxy_provider(resolved_config)
270469

271470

272471
def create_simple_jwt_provider(

0 commit comments

Comments
 (0)