add environment variable support for google oauth client secret file generation with fallback to file based

This commit is contained in:
Taylor Wilsdon
2025-06-28 12:06:36 -07:00
parent bc2dcaf6f5
commit fef8479620
4 changed files with 158 additions and 23 deletions

View File

@@ -151,16 +151,81 @@ def load_credentials_from_session(session_id: str) -> Optional[Credentials]:
logger.debug(f"No credentials found in session cache for session_id: {session_id}")
return credentials
def load_client_secrets_from_env() -> Optional[Dict[str, Any]]:
"""
Loads the client secrets from environment variables.
Environment variables used:
- GOOGLE_OAUTH_CLIENT_ID: OAuth 2.0 client ID
- GOOGLE_OAUTH_CLIENT_SECRET: OAuth 2.0 client secret
- GOOGLE_OAUTH_REDIRECT_URI: (optional) OAuth redirect URI
Returns:
Client secrets configuration dict compatible with Google OAuth library,
or None if required environment variables are not set.
"""
client_id = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
client_secret = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
redirect_uri = os.getenv("GOOGLE_OAUTH_REDIRECT_URI")
if client_id and client_secret:
# Create config structure that matches Google client secrets format
web_config = {
"client_id": client_id,
"client_secret": client_secret,
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs"
}
# Add redirect_uri if provided via environment variable
if redirect_uri:
web_config["redirect_uris"] = [redirect_uri]
# Return the full config structure expected by Google OAuth library
config = {"web": web_config}
logger.info("Loaded OAuth client credentials from environment variables")
return config
logger.debug("OAuth client credentials not found in environment variables")
return None
def load_client_secrets(client_secrets_path: str) -> Dict[str, Any]:
"""Loads the client secrets file."""
"""
Loads the client secrets from environment variables (preferred) or from the client secrets file.
Priority order:
1. Environment variables (GOOGLE_OAUTH_CLIENT_ID, GOOGLE_OAUTH_CLIENT_SECRET)
2. File-based credentials at the specified path
Args:
client_secrets_path: Path to the client secrets JSON file (used as fallback)
Returns:
Client secrets configuration dict
Raises:
ValueError: If client secrets file has invalid format
IOError: If file cannot be read and no environment variables are set
"""
# First, try to load from environment variables
env_config = load_client_secrets_from_env()
if env_config:
# Extract the "web" config from the environment structure
return env_config["web"]
# Fall back to loading from file
try:
with open(client_secrets_path, 'r') as f:
client_config = json.load(f)
# The file usually contains a top-level key like "web" or "installed"
if "web" in client_config:
logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
return client_config["web"]
elif "installed" in client_config:
return client_config["installed"]
logger.info(f"Loaded OAuth client credentials from file: {client_secrets_path}")
return client_config["installed"]
else:
logger.error(f"Client secrets file {client_secrets_path} has unexpected format.")
raise ValueError("Invalid client secrets file format")
@@ -168,6 +233,34 @@ def load_client_secrets(client_secrets_path: str) -> Dict[str, Any]:
logger.error(f"Error loading client secrets file {client_secrets_path}: {e}")
raise
def create_oauth_flow(scopes: List[str], redirect_uri: str, state: Optional[str] = None) -> Flow:
"""Creates an OAuth flow using environment variables or client secrets file."""
# Try environment variables first
env_config = load_client_secrets_from_env()
if env_config:
# Use client config directly
flow = Flow.from_client_config(
env_config,
scopes=scopes,
redirect_uri=redirect_uri,
state=state
)
logger.debug("Created OAuth flow from environment variables")
return flow
# Fall back to file-based config
if not os.path.exists(CONFIG_CLIENT_SECRETS_PATH):
raise FileNotFoundError(f"OAuth client secrets file not found at {CONFIG_CLIENT_SECRETS_PATH} and no environment variables set")
flow = Flow.from_client_secrets_file(
CONFIG_CLIENT_SECRETS_PATH,
scopes=scopes,
redirect_uri=redirect_uri,
state=state
)
logger.debug(f"Created OAuth flow from client secrets file: {CONFIG_CLIENT_SECRETS_PATH}")
return flow
# --- Core OAuth Logic ---
async def start_auth_flow(
@@ -206,8 +299,7 @@ async def start_auth_flow(
OAUTH_STATE_TO_SESSION_ID_MAP[oauth_state] = mcp_session_id
logger.info(f"[start_auth_flow] Stored mcp_session_id '{mcp_session_id}' for oauth_state '{oauth_state}'.")
flow = Flow.from_client_secrets_file(
CONFIG_CLIENT_SECRETS_PATH, # Use module constant
flow = create_oauth_flow(
scopes=SCOPES, # Use global SCOPES
redirect_uri=redirect_uri, # Use passed redirect_uri
state=oauth_state
@@ -240,7 +332,7 @@ async def start_auth_flow(
return "\n".join(message_lines)
except FileNotFoundError as e:
error_text = f"OAuth client secrets file not found: {e}. Please ensure '{CONFIG_CLIENT_SECRETS_PATH}' is correctly configured."
error_text = f"OAuth client credentials not found: {e}. Please either:\n1. Set environment variables: GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET\n2. Ensure '{CONFIG_CLIENT_SECRETS_PATH}' file exists"
logger.error(error_text, exc_info=True)
raise Exception(error_text)
except Exception as e:
@@ -249,12 +341,12 @@ async def start_auth_flow(
raise Exception(error_text)
def handle_auth_callback(
client_secrets_path: str,
scopes: List[str],
authorization_response: str,
redirect_uri: str, # Made redirect_uri a required parameter
redirect_uri: str,
credentials_base_dir: str = DEFAULT_CREDENTIALS_DIR,
session_id: Optional[str] = None
session_id: Optional[str] = None,
client_secrets_path: Optional[str] = None # Deprecated: kept for backward compatibility
) -> Tuple[str, Credentials]:
"""
Handles the callback from Google, exchanges the code for credentials,
@@ -262,12 +354,12 @@ def handle_auth_callback(
and returns them.
Args:
client_secrets_path: Path to the Google client secrets JSON file.
scopes: List of OAuth scopes requested.
authorization_response: The full callback URL from Google.
redirect_uri: The redirect URI.
credentials_base_dir: Base directory for credential files.
session_id: Optional MCP session ID to associate with the credentials.
client_secrets_path: (Deprecated) Path to client secrets file. Ignored if environment variables are set.
Returns:
A tuple containing the user_google_email and the obtained Credentials object.
@@ -278,13 +370,16 @@ def handle_auth_callback(
HttpError: If fetching user info fails.
"""
try:
# Log deprecation warning if old parameter is used
if client_secrets_path:
logger.warning("The 'client_secrets_path' parameter is deprecated. Use GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables instead.")
# Allow HTTP for localhost in development
if 'OAUTHLIB_INSECURE_TRANSPORT' not in os.environ:
logger.warning("OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost development.")
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
flow = Flow.from_client_secrets_file(
client_secrets_path,
flow = create_oauth_flow(
scopes=scopes,
redirect_uri=redirect_uri
)