2025-04-27 12:34:22 -04:00
# auth/google_auth.py
2025-07-10 14:05:08 -04:00
import asyncio
2025-04-27 12:34:22 -04:00
import json
2025-07-10 14:05:08 -04:00
import jwt
2025-04-27 12:34:22 -04:00
import logging
2025-05-23 11:22:23 -04:00
import os
2025-05-10 17:57:25 -04:00
2025-07-10 14:05:08 -04:00
from typing import List , Optional , Tuple , Dict , Any
2025-09-28 16:08:41 -04:00
from urllib.parse import parse_qs , urlparse
2025-07-10 14:05:08 -04:00
2025-04-27 12:34:22 -04:00
from google.oauth2.credentials import Credentials
2025-07-10 14:05:08 -04:00
from google_auth_oauthlib.flow import Flow
2025-04-27 12:34:22 -04:00
from google.auth.transport.requests import Request
2025-06-08 12:04:19 -04:00
from google.auth.exceptions import RefreshError
2025-04-27 12:34:22 -04:00
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
2025-08-24 11:15:11 -04:00
from auth.scopes import SCOPES , get_current_scopes # noqa
2025-08-03 10:30:04 -04:00
from auth.oauth21_session_store import get_oauth21_session_store
2025-08-10 18:11:27 -04:00
from auth.credential_store import get_credential_store
2025-08-23 12:04:04 -04:00
from auth.oauth_config import get_oauth_config , is_stateless_mode
2025-08-03 10:30:04 -04:00
from core.config import (
get_transport_mode ,
get_oauth_redirect_uri ,
)
from core.context import get_fastmcp_session_id
# Try to import FastMCP dependencies (may not be available in all environments)
try :
from fastmcp.server.dependencies import get_context as get_fastmcp_context
except ImportError :
get_fastmcp_context = None
2025-05-13 12:36:53 -04:00
2025-04-27 12:34:22 -04:00
# Configure logging
logging . basicConfig ( level = logging . INFO )
logger = logging . getLogger ( __name__ )
2025-07-04 16:10:01 -04:00
2025-04-27 12:34:22 -04:00
# Constants
2025-07-04 16:10:01 -04:00
def get_default_credentials_dir ():
"""Get the default credentials directory path, preferring user-specific locations."""
# Check for explicit environment variable override
if os . getenv ( "GOOGLE_MCP_CREDENTIALS_DIR" ):
return os . getenv ( "GOOGLE_MCP_CREDENTIALS_DIR" )
# Use user home directory for credentials storage
home_dir = os . path . expanduser ( "~" )
if home_dir and home_dir != "~" : # Valid home directory found
return os . path . join ( home_dir , ".google_workspace_mcp" , "credentials" )
# Fallback to current working directory if home directory is not accessible
return os . path . join ( os . getcwd (), ".credentials" )
DEFAULT_CREDENTIALS_DIR = get_default_credentials_dir ()
2025-05-10 17:57:25 -04:00
2025-08-03 11:12:58 -04:00
# Session credentials now handled by OAuth21SessionStore - no local cache needed
2025-05-13 12:36:53 -04:00
# Centralized Client Secrets Path Logic
2025-07-04 16:10:01 -04:00
_client_secrets_env = os . getenv ( "GOOGLE_CLIENT_SECRET_PATH" ) or os . getenv (
"GOOGLE_CLIENT_SECRETS"
)
2025-05-13 12:36:53 -04:00
if _client_secrets_env :
CONFIG_CLIENT_SECRETS_PATH = _client_secrets_env
else :
# Assumes this file is in auth/ and client_secret.json is in the root
CONFIG_CLIENT_SECRETS_PATH = os . path . join (
os . path . dirname ( os . path . dirname ( os . path . abspath ( __file__ ))),
2025-07-04 16:10:01 -04:00
"client_secret.json" ,
2025-05-13 12:36:53 -04:00
)
2025-05-11 17:39:15 -04:00
2025-04-27 12:34:22 -04:00
# --- Helper Functions ---
2025-07-04 16:10:01 -04:00
def _find_any_credentials (
base_dir : str = DEFAULT_CREDENTIALS_DIR ,
) -> Optional [ Credentials ]:
2025-05-23 11:22:23 -04:00
"""
Find and load any valid credentials from the credentials directory.
Used in single-user mode to bypass session-to-OAuth mapping.
Returns:
First valid Credentials object found, or None if none exist.
"""
2025-08-10 18:11:27 -04:00
try :
store = get_credential_store ()
users = store . list_users ()
if not users :
logger . info (
"[single-user] No users found with credentials via credential store"
)
return None
2025-04-27 12:34:22 -04:00
2025-08-10 18:11:27 -04:00
# Return credentials for the first user found
first_user = users [ 0 ]
credentials = store . get_credential ( first_user )
if credentials :
logger . info (
f "[single-user] Found credentials for { first_user } via credential store"
)
return credentials
else :
logger . warning (
f "[single-user] Could not load credentials for { first_user } via credential store"
)
2025-07-04 16:10:01 -04:00
2025-08-10 18:11:27 -04:00
except Exception as e :
2025-07-04 16:10:01 -04:00
logger . error (
2025-08-10 18:11:27 -04:00
f "[single-user] Error finding credentials via credential store: { e } "
2025-07-04 16:10:01 -04:00
)
2025-08-10 18:11:27 -04:00
logger . info ( "[single-user] No valid credentials found via credential store" )
return None
2025-04-27 12:34:22 -04:00
2025-07-04 16:10:01 -04:00
2025-05-11 17:39:15 -04:00
def save_credentials_to_session ( session_id : str , credentials : Credentials ):
2025-08-03 11:12:58 -04:00
"""Saves user credentials using OAuth21SessionStore."""
# Get user email from credentials if possible
user_email = None
if credentials and credentials . id_token :
try :
decoded_token = jwt . decode (
credentials . id_token , options = { "verify_signature" : False }
)
user_email = decoded_token . get ( "email" )
except Exception as e :
logger . debug ( f "Could not decode id_token to get email: { e } " )
2025-08-10 18:11:27 -04:00
2025-08-03 11:12:58 -04:00
if user_email :
store = get_oauth21_session_store ()
store . store_session (
user_email = user_email ,
access_token = credentials . token ,
refresh_token = credentials . refresh_token ,
token_uri = credentials . token_uri ,
client_id = credentials . client_id ,
client_secret = credentials . client_secret ,
scopes = credentials . scopes ,
expiry = credentials . expiry ,
mcp_session_id = session_id
)
logger . debug ( f "Credentials saved to OAuth21SessionStore for session_id: { session_id } , user: { user_email } " )
else :
logger . warning ( f "Could not save credentials to session store - no user email found for session: { session_id } " )
2025-05-11 17:39:15 -04:00
2025-07-04 16:10:01 -04:00
2025-05-11 17:39:15 -04:00
def load_credentials_from_session ( session_id : str ) -> Optional [ Credentials ]:
2025-08-03 11:12:58 -04:00
"""Loads user credentials from OAuth21SessionStore."""
store = get_oauth21_session_store ()
credentials = store . get_credentials_by_mcp_session ( session_id )
2025-05-11 17:39:15 -04:00
if credentials :
2025-07-04 16:10:01 -04:00
logger . debug (
2025-08-03 11:12:58 -04:00
f "Credentials loaded from OAuth21SessionStore for session_id: { session_id } "
2025-07-04 16:10:01 -04:00
)
2025-05-11 17:39:15 -04:00
else :
2025-07-04 16:10:01 -04:00
logger . debug (
2025-08-03 11:12:58 -04:00
f "No credentials found in OAuth21SessionStore for session_id: { session_id } "
2025-07-04 16:10:01 -04:00
)
2025-05-11 17:39:15 -04:00
return credentials
2025-07-04 16:10:01 -04:00
2025-06-28 12:06:36 -07:00
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" ,
2025-07-04 16:10:01 -04:00
"auth_provider_x509_cert_url" : "https://www.googleapis.com/oauth2/v1/certs" ,
2025-06-28 12:06:36 -07:00
}
# 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
2025-07-04 16:10:01 -04:00
2025-04-27 12:34:22 -04:00
def load_client_secrets ( client_secrets_path : str ) -> Dict [ str , Any ]:
2025-06-28 12:06:36 -07:00
"""
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
2025-04-27 12:34:22 -04:00
try :
2025-07-04 16:10:01 -04:00
with open ( client_secrets_path , "r" ) as f :
2025-04-27 12:34:22 -04:00
client_config = json . load ( f )
# The file usually contains a top-level key like "web" or "installed"
if "web" in client_config :
2025-07-04 16:10:01 -04:00
logger . info (
f "Loaded OAuth client credentials from file: { client_secrets_path } "
)
2025-04-27 12:34:22 -04:00
return client_config [ "web" ]
elif "installed" in client_config :
2025-07-04 16:10:01 -04:00
logger . info (
f "Loaded OAuth client credentials from file: { client_secrets_path } "
)
2025-06-28 12:06:36 -07:00
return client_config [ "installed" ]
2025-04-27 12:34:22 -04:00
else :
2025-07-04 16:10:01 -04:00
logger . error (
f "Client secrets file { client_secrets_path } has unexpected format."
)
raise ValueError ( "Invalid client secrets file format" )
2025-04-27 12:34:22 -04:00
except ( IOError , json . JSONDecodeError ) as e :
logger . error ( f "Error loading client secrets file { client_secrets_path } : { e } " )
raise
2025-07-04 16:10:01 -04:00
2025-06-28 12:56:43 -07:00
def check_client_secrets () -> Optional [ str ]:
"""
Checks for the presence of OAuth client secrets, either as environment
variables or as a file.
Returns:
An error message string if secrets are not found, otherwise None.
"""
env_config = load_client_secrets_from_env ()
if not env_config and not os . path . exists ( CONFIG_CLIENT_SECRETS_PATH ):
2025-07-04 16:10:01 -04:00
logger . error (
f "OAuth client credentials not found. No environment variables set and no file at { CONFIG_CLIENT_SECRETS_PATH } "
)
2025-06-28 13:33:44 -07:00
return f "OAuth client credentials not found. Please set GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables or provide a client secrets file at { CONFIG_CLIENT_SECRETS_PATH } ."
2025-06-28 12:56:43 -07:00
return None
2025-04-27 12:34:22 -04:00
2025-07-04 16:10:01 -04:00
def create_oauth_flow (
scopes : List [ str ], redirect_uri : str , state : Optional [ str ] = None
) -> Flow :
2025-06-28 12:06:36 -07:00
"""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 (
2025-07-04 16:10:01 -04:00
env_config , scopes = scopes , redirect_uri = redirect_uri , state = state
2025-06-28 12:06:36 -07:00
)
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 ):
2025-07-04 16:10:01 -04:00
raise FileNotFoundError (
f "OAuth client secrets file not found at { CONFIG_CLIENT_SECRETS_PATH } and no environment variables set"
)
2025-06-28 12:06:36 -07:00
flow = Flow . from_client_secrets_file (
CONFIG_CLIENT_SECRETS_PATH ,
scopes = scopes ,
redirect_uri = redirect_uri ,
2025-07-04 16:10:01 -04:00
state = state ,
)
logger . debug (
f "Created OAuth flow from client secrets file: { CONFIG_CLIENT_SECRETS_PATH } "
2025-06-28 12:06:36 -07:00
)
return flow
2025-07-04 16:10:01 -04:00
2025-04-27 12:34:22 -04:00
# --- Core OAuth Logic ---
2025-07-04 16:10:01 -04:00
2025-05-13 12:36:53 -04:00
async def start_auth_flow (
user_google_email : Optional [ str ],
2025-07-04 16:10:01 -04:00
service_name : str , # e.g., "Google Calendar", "Gmail" for user messages
redirect_uri : str , # Added redirect_uri as a required parameter
2025-06-06 18:51:34 -04:00
) -> str :
2025-04-27 12:34:22 -04:00
"""
2025-05-13 12:36:53 -04:00
Initiates the Google OAuth flow and returns an actionable message for the user.
2025-04-27 12:34:22 -04:00
Args:
2025-05-13 12:36:53 -04:00
user_google_email: The user's specified Google email, if provided.
service_name: The name of the Google service requiring auth (for user messages).
2025-04-27 12:34:22 -04:00
redirect_uri: The URI Google will redirect to after authorization.
Returns:
2025-06-06 18:51:34 -04:00
A formatted string containing guidance for the LLM/user.
Raises:
Exception: If the OAuth flow cannot be initiated.
2025-04-27 12:34:22 -04:00
"""
2025-07-04 16:10:01 -04:00
initial_email_provided = bool (
user_google_email
and user_google_email . strip ()
and user_google_email . lower () != "default"
)
user_display_name = (
f " { service_name } for ' { user_google_email } '"
if initial_email_provided
else service_name
)
2025-05-13 12:36:53 -04:00
2025-07-04 16:10:01 -04:00
logger . info (
2025-08-24 10:37:04 -04:00
f "[start_auth_flow] Initiating auth for { user_display_name } with scopes for enabled tools."
2025-07-04 16:10:01 -04:00
)
2025-05-13 12:36:53 -04:00
2025-08-03 10:30:04 -04:00
# Note: Caller should ensure OAuth callback is available before calling this function
2025-07-26 16:20:57 -04:00
2025-04-27 12:34:22 -04:00
try :
2025-07-04 16:10:01 -04:00
if "OAUTHLIB_INSECURE_TRANSPORT" not in os . environ and (
"localhost" in redirect_uri or "127.0.0.1" in redirect_uri
): # Use passed redirect_uri
logger . warning (
"OAUTHLIB_INSECURE_TRANSPORT not set. Setting it for localhost/local development."
)
os . environ [ "OAUTHLIB_INSECURE_TRANSPORT" ] = "1"
2025-05-10 17:57:25 -04:00
2025-05-13 12:36:53 -04:00
oauth_state = os . urandom ( 16 ) . hex ()
2025-06-28 12:06:36 -07:00
flow = create_oauth_flow (
2025-08-24 10:37:04 -04:00
scopes = get_current_scopes (), # Use scopes for enabled tools only
2025-07-04 16:10:01 -04:00
redirect_uri = redirect_uri , # Use passed redirect_uri
state = oauth_state ,
2025-04-27 12:34:22 -04:00
)
2025-07-04 16:10:01 -04:00
auth_url , _ = flow . authorization_url ( access_type = "offline" , prompt = "consent" )
2025-09-28 16:08:41 -04:00
session_id = None
try :
session_id = get_fastmcp_session_id ()
except Exception as e :
logger . debug ( f "Could not retrieve FastMCP session ID for state binding: { e } " )
store = get_oauth21_session_store ()
store . store_oauth_state ( oauth_state , session_id = session_id )
2025-07-04 16:10:01 -04:00
logger . info (
2025-09-28 16:08:41 -04:00
f "Auth flow started for { user_display_name } . State: { oauth_state [: 8 ] } ... Advise user to visit: { auth_url } "
2025-07-04 16:10:01 -04:00
)
2025-05-13 12:36:53 -04:00
message_lines = [
f "**ACTION REQUIRED: Google Authentication Needed for { user_display_name } ** \n " ,
f "To proceed, the user must authorize this application for { service_name } access using all required permissions." ,
"**LLM, please present this exact authorization URL to the user as a clickable hyperlink:**" ,
f "Authorization URL: { auth_url } " ,
f "Markdown for hyperlink: [Click here to authorize { service_name } access]( { auth_url } ) \n " ,
"**LLM, after presenting the link, instruct the user as follows:**" ,
"1. Click the link and complete the authorization in their browser." ,
]
2025-07-26 12:05:22 -04:00
session_info_for_llm = ""
2025-05-13 12:36:53 -04:00
if not initial_email_provided :
2025-07-04 16:10:01 -04:00
message_lines . extend (
[
f "2. After successful authorization { session_info_for_llm } , the browser page will display the authenticated email address." ,
" **LLM: Instruct the user to provide you with this email address.**" ,
"3. Once you have the email, **retry their original command, ensuring you include this `user_google_email`.**" ,
]
)
2025-05-13 12:36:53 -04:00
else :
2025-07-04 16:10:01 -04:00
message_lines . append (
f "2. After successful authorization { session_info_for_llm } , **retry their original command**."
)
2025-05-13 12:36:53 -04:00
2025-07-04 16:10:01 -04:00
message_lines . append (
f " \n The application will use the new credentials. If ' { user_google_email } ' was provided, it must match the authenticated account."
)
2025-06-06 18:51:34 -04:00
return " \n " . join ( message_lines )
2025-05-13 12:36:53 -04:00
except FileNotFoundError as e :
2025-06-28 12:06:36 -07:00
error_text = f "OAuth client credentials not found: { e } . Please either: \n 1. Set environment variables: GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET \n 2. Ensure ' { CONFIG_CLIENT_SECRETS_PATH } ' file exists"
2025-05-13 12:36:53 -04:00
logger . error ( error_text , exc_info = True )
2025-06-06 18:51:34 -04:00
raise Exception ( error_text )
2025-04-27 12:34:22 -04:00
except Exception as e :
2025-05-13 12:36:53 -04:00
error_text = f "Could not initiate authentication for { user_display_name } due to an unexpected error: { str ( e ) } "
2025-07-04 16:10:01 -04:00
logger . error (
f "Failed to start the OAuth flow for { user_display_name } : { e } " ,
exc_info = True ,
)
2025-06-06 18:51:34 -04:00
raise Exception ( error_text )
2025-04-27 12:34:22 -04:00
2025-07-04 16:10:01 -04:00
2025-04-27 12:34:22 -04:00
def handle_auth_callback (
scopes : List [ str ],
authorization_response : str ,
2025-06-28 12:06:36 -07:00
redirect_uri : str ,
2025-05-11 17:39:15 -04:00
credentials_base_dir : str = DEFAULT_CREDENTIALS_DIR ,
2025-06-28 12:06:36 -07:00
session_id : Optional [ str ] = None ,
2025-07-04 16:10:01 -04:00
client_secrets_path : Optional [
str
] = None , # Deprecated: kept for backward compatibility
2025-04-27 12:34:22 -04:00
) -> Tuple [ str , Credentials ]:
"""
Handles the callback from Google, exchanges the code for credentials,
2025-05-11 17:39:15 -04:00
fetches user info, determines user_google_email, saves credentials (file & session),
and returns them.
2025-04-27 12:34:22 -04:00
Args:
2025-05-11 17:39:15 -04:00
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.
2025-06-28 12:06:36 -07:00
client_secrets_path: (Deprecated) Path to client secrets file. Ignored if environment variables are set.
2025-04-27 12:34:22 -04:00
Returns:
2025-05-11 17:39:15 -04:00
A tuple containing the user_google_email and the obtained Credentials object.
2025-04-27 12:34:22 -04:00
Raises:
ValueError: If the state is missing or doesn't match.
FlowExchangeError: If the code exchange fails.
HttpError: If fetching user info fails.
"""
try :
2025-06-28 12:06:36 -07:00
# Log deprecation warning if old parameter is used
if client_secrets_path :
2025-07-04 16:10:01 -04:00
logger . warning (
"The 'client_secrets_path' parameter is deprecated. Use GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET environment variables instead."
)
2025-06-28 12:06:36 -07:00
2025-05-10 17:57:25 -04:00
# Allow HTTP for localhost in development
2025-07-04 16:10:01 -04:00
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"
2025-05-10 17:57:25 -04:00
2025-09-28 16:08:41 -04:00
store = get_oauth21_session_store ()
parsed_response = urlparse ( authorization_response )
state_values = parse_qs ( parsed_response . query ) . get ( "state" )
state = state_values [ 0 ] if state_values else None
state_info = store . validate_and_consume_oauth_state ( state , session_id = session_id )
logger . debug (
"Validated OAuth callback state %s for session %s " ,
( state [: 8 ] if state else "<missing>" ),
state_info . get ( "session_id" ) or "<unknown>" ,
)
flow = create_oauth_flow (
scopes = scopes , redirect_uri = redirect_uri , state = state
)
2025-04-27 12:34:22 -04:00
# Exchange the authorization code for credentials
# Note: fetch_token will use the redirect_uri configured in the flow
flow . fetch_token ( authorization_response = authorization_response )
credentials = flow . credentials
logger . info ( "Successfully exchanged authorization code for tokens." )
# Get user info to determine user_id (using email here)
user_info = get_user_info ( credentials )
2025-07-04 16:10:01 -04:00
if not user_info or "email" not in user_info :
logger . error ( "Could not retrieve user email from Google." )
raise ValueError ( "Failed to get user email for identification." )
2025-04-27 12:34:22 -04:00
2025-07-04 16:10:01 -04:00
user_google_email = user_info [ "email" ]
2025-05-11 17:39:15 -04:00
logger . info ( f "Identified user_google_email: { user_google_email } " )
2025-04-27 12:34:22 -04:00
2025-08-10 18:11:27 -04:00
# Save the credentials
credential_store = get_credential_store ()
credential_store . store_credential ( user_google_email , credentials )
2025-04-27 12:34:22 -04:00
2025-08-03 11:12:58 -04:00
# Always save to OAuth21SessionStore for centralized management
store = get_oauth21_session_store ()
store . store_session (
user_email = user_google_email ,
access_token = credentials . token ,
refresh_token = credentials . refresh_token ,
token_uri = credentials . token_uri ,
client_id = credentials . client_id ,
client_secret = credentials . client_secret ,
scopes = credentials . scopes ,
expiry = credentials . expiry ,
2025-08-05 10:22:01 -04:00
mcp_session_id = session_id ,
issuer = "https://accounts.google.com" # Add issuer for Google tokens
2025-08-03 11:12:58 -04:00
)
# If session_id is provided, also save to session cache for compatibility
2025-05-11 17:39:15 -04:00
if session_id :
save_credentials_to_session ( session_id , credentials )
return user_google_email , credentials
2025-04-27 12:34:22 -04:00
2025-07-04 16:10:01 -04:00
except Exception as e : # Catch specific exceptions like FlowExchangeError if needed
2025-04-27 12:34:22 -04:00
logger . error ( f "Error handling auth callback: { e } " )
2025-07-04 16:10:01 -04:00
raise # Re-raise for the caller
2025-04-27 12:34:22 -04:00
def get_credentials (
2025-07-04 16:10:01 -04:00
user_google_email : Optional [ str ], # Can be None if relying on session_id
2025-04-27 12:34:22 -04:00
required_scopes : List [ str ],
2025-05-11 17:39:15 -04:00
client_secrets_path : Optional [ str ] = None ,
credentials_base_dir : str = DEFAULT_CREDENTIALS_DIR ,
2025-07-04 16:10:01 -04:00
session_id : Optional [ str ] = None ,
2025-04-27 12:34:22 -04:00
) -> Optional [ Credentials ]:
"""
2025-08-02 18:25:08 -04:00
Retrieves stored credentials, prioritizing OAuth 2.1 store, then session, then file. Refreshes if necessary.
2025-05-11 17:39:15 -04:00
If credentials are loaded from file and a session_id is present, they are cached in the session.
2025-05-23 11:22:23 -04:00
In single-user mode, bypasses session mapping and uses any available credentials.
2025-04-27 12:34:22 -04:00
Args:
2025-05-11 17:39:15 -04:00
user_google_email: Optional user's Google email.
2025-04-27 12:34:22 -04:00
required_scopes: List of scopes the credentials must have.
2025-05-11 17:39:15 -04:00
client_secrets_path: Path to client secrets, required for refresh if not in creds.
credentials_base_dir: Base directory for credential files.
session_id: Optional MCP session ID.
2025-04-27 12:34:22 -04:00
Returns:
2025-05-11 17:39:15 -04:00
Valid Credentials object or None.
2025-04-27 12:34:22 -04:00
"""
2025-08-02 18:25:08 -04:00
# First, try OAuth 2.1 session store if we have a session_id (FastMCP session)
if session_id :
try :
store = get_oauth21_session_store ()
2025-08-03 10:30:04 -04:00
2025-08-02 18:25:08 -04:00
# Try to get credentials by MCP session
credentials = store . get_credentials_by_mcp_session ( session_id )
if credentials :
logger . info ( f "[get_credentials] Found OAuth 2.1 credentials for MCP session { session_id } " )
2025-08-03 10:30:04 -04:00
2025-08-02 18:25:08 -04:00
# Check scopes
if not all ( scope in credentials . scopes for scope in required_scopes ):
logger . warning (
f "[get_credentials] OAuth 2.1 credentials lack required scopes. Need: { required_scopes } , Have: { credentials . scopes } "
)
return None
2025-08-03 10:30:04 -04:00
2025-08-02 18:25:08 -04:00
# Return if valid
if credentials . valid :
return credentials
elif credentials . expired and credentials . refresh_token :
# Try to refresh
try :
credentials . refresh ( Request ())
logger . info ( f "[get_credentials] Refreshed OAuth 2.1 credentials for session { session_id } " )
# Update stored credentials
user_email = store . get_user_by_mcp_session ( session_id )
if user_email :
store . store_session (
user_email = user_email ,
access_token = credentials . token ,
refresh_token = credentials . refresh_token ,
scopes = credentials . scopes ,
expiry = credentials . expiry ,
mcp_session_id = session_id
)
return credentials
except Exception as e :
logger . error ( f "[get_credentials] Failed to refresh OAuth 2.1 credentials: { e } " )
return None
except ImportError :
pass # OAuth 2.1 store not available
except Exception as e :
logger . debug ( f "[get_credentials] Error checking OAuth 2.1 store: { e } " )
2025-08-03 10:30:04 -04:00
2025-05-23 11:22:23 -04:00
# Check for single-user mode
2025-07-04 16:10:01 -04:00
if os . getenv ( "MCP_SINGLE_USER_MODE" ) == "1" :
logger . info (
2025-07-18 18:04:06 -04:00
"[get_credentials] Single-user mode: bypassing session mapping, finding any credentials"
2025-07-04 16:10:01 -04:00
)
2025-05-23 11:22:23 -04:00
credentials = _find_any_credentials ( credentials_base_dir )
if not credentials :
2025-07-04 16:10:01 -04:00
logger . info (
f "[get_credentials] Single-user mode: No credentials found in { credentials_base_dir } "
)
2025-05-23 11:22:23 -04:00
return None
# In single-user mode, if user_google_email wasn't provided, try to get it from user info
# This is needed for proper credential saving after refresh
if not user_google_email and credentials . valid :
try :
user_info = get_user_info ( credentials )
2025-07-04 16:10:01 -04:00
if user_info and "email" in user_info :
user_google_email = user_info [ "email" ]
logger . debug (
f "[get_credentials] Single-user mode: extracted user email { user_google_email } from credentials"
)
2025-05-23 11:22:23 -04:00
except Exception as e :
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Single-user mode: could not extract user email: { e } "
)
2025-05-23 11:22:23 -04:00
else :
credentials : Optional [ Credentials ] = None
2025-05-30 11:09:56 -04:00
# Session ID should be provided by the caller
2025-05-23 11:22:23 -04:00
if not session_id :
2025-05-30 11:09:56 -04:00
logger . debug ( "[get_credentials] No session_id provided" )
2025-05-23 11:22:23 -04:00
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Called for user_google_email: ' { user_google_email } ', session_id: ' { session_id } ', required_scopes: { required_scopes } "
)
2025-05-23 11:22:23 -04:00
if session_id :
credentials = load_credentials_from_session ( session_id )
if credentials :
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Loaded credentials from session for session_id ' { session_id } '."
)
2025-05-23 11:22:23 -04:00
if not credentials and user_google_email :
2025-08-23 11:12:21 -04:00
if not is_stateless_mode ():
logger . debug (
f "[get_credentials] No session credentials, trying credential store for user_google_email ' { user_google_email } '."
)
store = get_credential_store ()
credentials = store . get_credential ( user_google_email )
else :
logger . debug (
f "[get_credentials] No session credentials, skipping file store in stateless mode for user_google_email ' { user_google_email } '."
)
2025-08-10 18:11:27 -04:00
2025-05-23 11:22:23 -04:00
if credentials and session_id :
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Loaded from file for user ' { user_google_email } ', caching to session ' { session_id } '."
)
save_credentials_to_session (
session_id , credentials
) # Cache for current session
2025-05-23 11:22:23 -04:00
if not credentials :
2025-07-04 16:10:01 -04:00
logger . info (
f "[get_credentials] No credentials found for user ' { user_google_email } ' or session ' { session_id } '."
)
2025-05-23 11:22:23 -04:00
return None
2025-05-13 12:36:53 -04:00
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Credentials found. Scopes: { credentials . scopes } , Valid: { credentials . valid } , Expired: { credentials . expired } "
)
2025-04-27 12:34:22 -04:00
if not all ( scope in credentials . scopes for scope in required_scopes ):
2025-07-04 16:10:01 -04:00
logger . warning (
f "[get_credentials] Credentials lack required scopes. Need: { required_scopes } , Have: { credentials . scopes } . User: ' { user_google_email } ', Session: ' { session_id } '"
)
return None # Re-authentication needed for scopes
2025-05-13 12:36:53 -04:00
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Credentials have sufficient scopes. User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-04-27 12:34:22 -04:00
if credentials . valid :
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Credentials are valid. User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-04-27 12:34:22 -04:00
return credentials
elif credentials . expired and credentials . refresh_token :
2025-07-04 16:10:01 -04:00
logger . info (
f "[get_credentials] Credentials expired. Attempting refresh. User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-04-27 12:34:22 -04:00
if not client_secrets_path :
2025-07-04 16:10:01 -04:00
logger . error (
"[get_credentials] Client secrets path required for refresh but not provided."
)
return None
2025-04-27 12:34:22 -04:00
try :
2025-07-04 16:10:01 -04:00
logger . debug (
f "[get_credentials] Refreshing token using client_secrets_path: { client_secrets_path } "
)
2025-05-11 17:39:15 -04:00
# client_config = load_client_secrets(client_secrets_path) # Not strictly needed if creds have client_id/secret
credentials . refresh ( Request ())
2025-07-04 16:10:01 -04:00
logger . info (
f "[get_credentials] Credentials refreshed successfully. User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-05-13 12:36:53 -04:00
2025-08-23 11:12:21 -04:00
# Save refreshed credentials (skip file save in stateless mode)
2025-08-10 18:11:27 -04:00
if user_google_email : # Always save to credential store if email is known
2025-08-23 11:12:21 -04:00
if not is_stateless_mode ():
credential_store = get_credential_store ()
credential_store . store_credential ( user_google_email , credentials )
else :
logger . info ( f "Skipping credential file save in stateless mode for { user_google_email } " )
2025-08-10 18:11:27 -04:00
2025-08-03 11:12:58 -04:00
# Also update OAuth21SessionStore
store = get_oauth21_session_store ()
store . store_session (
user_email = user_google_email ,
access_token = credentials . token ,
refresh_token = credentials . refresh_token ,
token_uri = credentials . token_uri ,
client_id = credentials . client_id ,
client_secret = credentials . client_secret ,
scopes = credentials . scopes ,
expiry = credentials . expiry ,
2025-08-05 10:22:01 -04:00
mcp_session_id = session_id ,
issuer = "https://accounts.google.com" # Add issuer for Google tokens
2025-08-03 11:12:58 -04:00
)
2025-08-10 18:11:27 -04:00
2025-07-04 16:10:01 -04:00
if session_id : # Update session cache if it was the source or is active
2025-05-11 17:39:15 -04:00
save_credentials_to_session ( session_id , credentials )
2025-04-27 12:34:22 -04:00
return credentials
2025-06-08 12:04:19 -04:00
except RefreshError as e :
2025-07-04 16:10:01 -04:00
logger . warning (
f "[get_credentials] RefreshError - token expired/revoked: { e } . User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-06-08 12:04:19 -04:00
# For RefreshError, we should return None to trigger reauthentication
return None
2025-05-11 17:39:15 -04:00
except Exception as e :
2025-07-04 16:10:01 -04:00
logger . error (
f "[get_credentials] Error refreshing credentials: { e } . User: ' { user_google_email } ', Session: ' { session_id } '" ,
exc_info = True ,
)
return None # Failed to refresh
2025-04-27 12:34:22 -04:00
else :
2025-07-04 16:10:01 -04:00
logger . warning (
f "[get_credentials] Credentials invalid/cannot refresh. Valid: { credentials . valid } , Refresh Token: { credentials . refresh_token is not None } . User: ' { user_google_email } ', Session: ' { session_id } '"
)
2025-04-27 12:34:22 -04:00
return None
def get_user_info ( credentials : Credentials ) -> Optional [ Dict [ str , Any ]]:
"""Fetches basic user profile information (requires userinfo.email scope)."""
if not credentials or not credentials . valid :
logger . error ( "Cannot get user info: Invalid or missing credentials." )
return None
try :
# Using googleapiclient discovery to get user info
# Requires 'google-api-python-client' library
2025-07-04 16:10:01 -04:00
service = build ( "oauth2" , "v2" , credentials = credentials )
2025-04-27 12:34:22 -04:00
user_info = service . userinfo () . get () . execute ()
logger . info ( f "Successfully fetched user info: { user_info . get ( 'email' ) } " )
return user_info
except HttpError as e :
logger . error ( f "HttpError fetching user info: { e . status_code } { e . reason } " )
# Handle specific errors, e.g., 401 Unauthorized might mean token issue
return None
except Exception as e :
logger . error ( f "Unexpected error fetching user info: { e } " )
return None
2025-05-24 10:43:55 -04:00
# --- Centralized Google Service Authentication ---
2025-07-04 16:10:01 -04:00
2025-06-06 18:51:34 -04:00
class GoogleAuthenticationError ( Exception ):
"""Exception raised when Google authentication is required or fails."""
2025-07-04 16:10:01 -04:00
2025-06-06 18:51:34 -04:00
def __init__ ( self , message : str , auth_url : Optional [ str ] = None ):
super () . __init__ ( message )
self . auth_url = auth_url
2025-05-24 10:43:55 -04:00
async def get_authenticated_google_service (
2025-07-04 16:10:01 -04:00
service_name : str , # "gmail", "calendar", "drive", "docs"
version : str , # "v1", "v3"
tool_name : str , # For logging/debugging
user_google_email : str , # Required - no more Optional
2025-05-24 10:43:55 -04:00
required_scopes : List [ str ],
2025-08-02 18:25:08 -04:00
session_id : Optional [ str ] = None , # Session context for logging
2025-06-06 18:51:34 -04:00
) -> tuple [ Any , str ]:
2025-05-24 10:43:55 -04:00
"""
Centralized Google service authentication for all MCP tools.
2025-06-06 18:51:34 -04:00
Returns (service, user_email) on success or raises GoogleAuthenticationError.
2025-05-24 10:43:55 -04:00
Args:
service_name: The Google service name ("gmail", "calendar", "drive", "docs")
version: The API version ("v1", "v3", etc.)
tool_name: The name of the calling tool (for logging/debugging)
user_google_email: The user's Google email address (required)
required_scopes: List of required OAuth scopes
Returns:
2025-06-06 18:51:34 -04:00
tuple[service, user_email] on success
Raises:
GoogleAuthenticationError: When authentication is required or fails
2025-05-24 10:43:55 -04:00
"""
2025-08-03 10:30:04 -04:00
2025-08-02 18:25:08 -04:00
# Try to get FastMCP session ID if not provided
if not session_id :
try :
# First try context variable (works in async context)
session_id = get_fastmcp_session_id ()
if session_id :
logger . debug ( f "[ { tool_name } ] Got FastMCP session ID from context: { session_id } " )
2025-08-02 18:50:49 -04:00
else :
logger . debug ( f "[ { tool_name } ] Context variable returned None/empty session ID" )
2025-08-02 18:25:08 -04:00
except Exception as e :
2025-08-10 18:11:27 -04:00
logger . debug (
f "[ { tool_name } ] Could not get FastMCP session from context: { e } "
)
2025-08-03 10:30:04 -04:00
2025-08-02 18:25:08 -04:00
# Fallback to direct FastMCP context if context variable not set
2025-08-03 10:30:04 -04:00
if not session_id and get_fastmcp_context :
2025-08-02 18:25:08 -04:00
try :
2025-08-03 10:30:04 -04:00
fastmcp_ctx = get_fastmcp_context ()
2025-08-02 18:50:49 -04:00
if fastmcp_ctx and hasattr ( fastmcp_ctx , 'session_id' ):
session_id = fastmcp_ctx . session_id
logger . debug ( f "[ { tool_name } ] Got FastMCP session ID directly: { session_id } " )
else :
logger . debug ( f "[ { tool_name } ] FastMCP context exists but no session_id attribute" )
2025-08-02 18:25:08 -04:00
except Exception as e :
logger . debug ( f "[ { tool_name } ] Could not get FastMCP context directly: { e } " )
2025-08-03 10:30:04 -04:00
2025-08-02 18:50:49 -04:00
# Final fallback: log if we still don't have session_id
if not session_id :
logger . warning ( f "[ { tool_name } ] Unable to obtain FastMCP session ID from any source" )
2025-08-03 10:30:04 -04:00
2025-05-24 10:43:55 -04:00
logger . info (
2025-08-02 18:25:08 -04:00
f "[ { tool_name } ] Attempting to get authenticated { service_name } service. Email: ' { user_google_email } ', Session: ' { session_id } '"
2025-05-24 10:43:55 -04:00
)
# Validate email format
if not user_google_email or "@" not in user_google_email :
error_msg = f "Authentication required for { tool_name } . No valid 'user_google_email' provided. Please provide a valid Google email address."
logger . info ( f "[ { tool_name } ] { error_msg } " )
2025-06-06 18:51:34 -04:00
raise GoogleAuthenticationError ( error_msg )
2025-05-24 10:43:55 -04:00
credentials = await asyncio . to_thread (
get_credentials ,
user_google_email = user_google_email ,
required_scopes = required_scopes ,
client_secrets_path = CONFIG_CLIENT_SECRETS_PATH ,
2025-08-02 18:25:08 -04:00
session_id = session_id , # Pass through session context
2025-05-24 10:43:55 -04:00
)
if not credentials or not credentials . valid :
2025-08-10 18:11:27 -04:00
logger . warning ( f "[ { tool_name } ] No valid credentials. Email: ' { user_google_email } '." )
logger . info ( f "[ { tool_name } ] Valid email ' { user_google_email } ' provided, initiating auth flow." )
2025-05-24 10:43:55 -04:00
2025-06-07 16:16:48 -04:00
# Ensure OAuth callback is available
2025-08-03 10:30:04 -04:00
from auth.oauth_callback_server import ensure_oauth_callback_available
2025-08-10 18:11:27 -04:00
2025-08-03 10:30:04 -04:00
redirect_uri = get_oauth_redirect_uri ()
2025-08-22 09:51:49 -04:00
config = get_oauth_config ()
2025-08-10 18:11:27 -04:00
success , error_msg = ensure_oauth_callback_available (
2025-08-22 09:51:49 -04:00
get_transport_mode (), config . port , config . base_uri
2025-08-10 18:11:27 -04:00
)
2025-08-03 10:30:04 -04:00
if not success :
error_detail = f " ( { error_msg } )" if error_msg else ""
2025-08-10 18:11:27 -04:00
raise GoogleAuthenticationError (
f "Cannot initiate OAuth flow - callback server unavailable { error_detail } "
)
2025-06-07 16:16:48 -04:00
2025-06-06 18:51:34 -04:00
# Generate auth URL and raise exception with it
auth_response = await start_auth_flow (
2025-05-24 10:43:55 -04:00
user_google_email = user_google_email ,
service_name = f "Google { service_name . title () } " ,
2025-06-07 16:00:55 -04:00
redirect_uri = redirect_uri ,
2025-05-24 10:43:55 -04:00
)
2025-06-07 16:16:48 -04:00
2025-06-06 18:51:34 -04:00
# Extract the auth URL from the response and raise with it
raise GoogleAuthenticationError ( auth_response )
2025-05-24 10:43:55 -04:00
try :
service = build ( service_name , version , credentials = credentials )
log_user_email = user_google_email
# Try to get email from credentials if needed for validation
if credentials and credentials . id_token :
try :
# Decode without verification (just to get email for logging)
2025-07-04 16:10:01 -04:00
decoded_token = jwt . decode (
credentials . id_token , options = { "verify_signature" : False }
)
2025-05-24 10:43:55 -04:00
token_email = decoded_token . get ( "email" )
if token_email :
log_user_email = token_email
logger . info ( f "[ { tool_name } ] Token email: { token_email } " )
except Exception as e :
logger . debug ( f "[ { tool_name } ] Could not decode id_token: { e } " )
2025-07-04 16:10:01 -04:00
logger . info (
f "[ { tool_name } ] Successfully authenticated { service_name } service for user: { log_user_email } "
)
2025-05-24 10:43:55 -04:00
return service , log_user_email
except Exception as e :
error_msg = f "[ { tool_name } ] Failed to build { service_name } service: { str ( e ) } "
logger . error ( error_msg , exc_info = True )
2025-06-06 18:51:34 -04:00
raise GoogleAuthenticationError ( error_msg )