@@ -254,19 +254,218 @@ def _create_remote_provider(config: RemoteAuthConfig) -> "AuthProvider":
254254
255255
256256def _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
272471def create_simple_jwt_provider (
0 commit comments