""" OAuth helper utilities for token exchange operations. Shared across multiple OAuth providers to reduce code duplication. """ import logging from typing import Any, Dict, Optional import requests from fastapi import HTTPException, status from app.config import settings logger = logging.getLogger(__name__) def exchange_oauth_token( provider_name: str, token_url: str, payload: Dict[str, str], timeout: Optional[int] = None ) -> Dict[str, Any]: """ Exchange an authorization code for tokens from an OAuth provider. This function handles the common OAuth token exchange flow across multiple providers (OneDrive, Google Drive, Dropbox) with proper error handling and secure logging. Args: provider_name: Name of the OAuth provider (for logging) token_url: OAuth token endpoint URL payload: Request payload containing client credentials and auth code timeout: Request timeout in seconds (defaults to settings.http_request_timeout) Returns: Dict containing the token response from the provider Raises: HTTPException: If token exchange fails or response is invalid """ if timeout is None: timeout = settings.http_request_timeout try: logger.info(f"Starting {provider_name} token exchange process") # SECURITY: Never log sensitive data - only log non-sensitive metadata safe_info = { "provider": provider_name, "token_url": token_url, "grant_type": payload.get("grant_type", "unknown"), } logger.info(f"Token exchange request: {safe_info}") # Make the token request logger.info(f"Sending POST request to {provider_name} for token exchange") response = requests.post(token_url, data=payload, timeout=timeout) # Check if the request was successful logger.info(f"Token exchange response status: {response.status_code}") if response.status_code != 200: # Log the error response for debugging (without sensitive data) try: error_json = response.json() # Extract only error type, not full details which may contain sensitive info error_type = error_json.get("error", "unknown_error") logger.error(f"Token exchange failed with status {response.status_code}: {error_type}") error_detail = {"error": error_type, "error_description": error_json.get("error_description", "")} except (ValueError, requests.exceptions.JSONDecodeError) as json_err: logger.error(f"Failed to parse error response as JSON: {str(json_err)}") error_detail = {"error": "Unknown error", "status_code": response.status_code} raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Token exchange failed: {error_detail}" ) # Parse the token response token_data = response.json() # Validate the token response if "refresh_token" not in token_data: logger.error(f"{provider_name} returned success but no refresh_token found in response") raise HTTPException( status_code=status.HTTP_502_BAD_GATEWAY, detail=f"{provider_name} OAuth server returned success but no refresh token was included", ) # Log success with non-sensitive metadata only logger.info(f"Successfully exchanged authorization code for {provider_name} tokens") return token_data except HTTPException: # Re-raise HTTP exceptions as they already have appropriate status codes raise except requests.exceptions.RequestException as e: logger.exception(f"Network error during {provider_name} token exchange: {str(e)}") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=f"Failed to connect to {provider_name} OAuth service: {str(e)}", ) except Exception as e: logger.exception(f"Unexpected error during {provider_name} token exchange: {str(e)}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to exchange token: {str(e)}" )