OAuth Integration Deep Dive

Master multi-provider OAuth integration in Fiberwise. Learn real-world patterns from a production email agent app including OAuth configuration, credential services, and secure token management across Gmail, Outlook, and Yahoo.

45-60 minutes Advanced OAuth + Agents

What You'll Learn

This tutorial explores real OAuth implementation patterns from a production email agent application:

  • OAuth Configuration: Multi-provider setup in app_manifest.yaml with proper scopes and parameters
  • Credential Service: Frontend OAuth flow handling with CredentialService class
  • Connection Management: Building UI components for OAuth connections
  • Agent Integration: Using OAuth credentials in Python agents with the execute() pattern
  • Multi-Provider Support: Unified abstraction for Gmail, Outlook, and Yahoo
  • Error Handling: Production-ready error recovery and token refresh

Real Production Code

All code examples in this tutorial are from the working email-agent-app in the fiber-apps repository. You can explore the full implementation there.

📋 Prerequisites: Your Setup Checklist

Before you begin, you need a fully configured Fiberwise environment. This is the foundation for building any app on the platform.

🔧 Required Setup

✅ All Set?

Once all boxes are checked, you are ready to proceed. If not, please complete the linked guides first.

Step 1: OAuth Architecture Overview

Fiberwise provides a complete OAuth infrastructure. Here's how the pieces fit together:

OAuth Flow in Fiberwise

1
User clicks "Connect"
->
2
CredentialService initiates OAuth
->
3
User authorizes at provider
->
4
Callback stores credentials
->
5
Agent uses oauth_service

Key Components

app_manifest.yaml

Defines OAuth authenticators, scopes, and provider configuration

credential-service.js

Frontend service for initiating OAuth flows and managing connections

email-connections.js

Web component UI for connecting/disconnecting OAuth providers

email_agent.py

Python agent using oauth_service for authenticated API requests

Step 2: OAuth Configuration in app_manifest.yaml

The app manifest is where you define OAuth authenticators. Here's the real configuration from the email-agent-app:

# app_manifest.yaml - OAuth configuration section
oauth:
  authenticators:
    - name: Gmail
      type: oauth2
      file: .fiber/local/oauth/fiberwise_developer.json
      scopes:
        - openid
        - profile
        - email
        - https://www.googleapis.com/auth/gmail.readonly
        - https://www.googleapis.com/auth/gmail.send
      additional_params:
        access_type: offline    # Request refresh token
        prompt: consent         # Always show consent screen

Configuration Breakdown

Field Purpose
name Display name for the OAuth provider
type Authentication type (oauth2 for OAuth 2.0)
file Path to OAuth credentials JSON (client_id, client_secret)
scopes OAuth scopes to request from the provider
additional_params Extra OAuth parameters (access_type, prompt, etc.)

Agent Permissions for OAuth

Your agent must declare OAuth permissions in the manifest:

# Agent section of app_manifest.yaml
agents:
  - name: email-agent
    implementation_path: agents/email_agent.py
    language: python
    agent_type_id: custom
    description: Agent for working with emails across different providers using OAuth
    permissions:
      - credentials.oauth    # Required for OAuth operations
      - data.read
      - data.write
      - llm.completion
    allowed_context_variables:
      - user_id
      - app_id
      - provider_id
      - template_name

OAuth Credentials File

The .fiber/local/oauth/fiberwise_developer.json file contains your OAuth client credentials. Never commit this file to version control. It should contain:

{
  "client_id": "YOUR_OAUTH_CLIENT_ID",
  "client_secret": "YOUR_OAUTH_CLIENT_SECRET",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token"
}

Step 3: The CredentialService Class

The CredentialService handles all OAuth operations on the frontend. Here's the real implementation:

/**
 * FiberWise Credential Service
 * Provides functionality for OAuth authentication and credential management
 */

class CredentialService {
  /**
   * Create a new Credential Service client
   * @param {Object} options - Configuration options
   * @param {FiberwiseAppConfig} options.config - Simple configuration instance
   * @param {string} options.appId - Optional app ID to override config
   * @param {string} options.baseUrl - Optional API base URL
   */
  constructor(options = {}) {
    this.config = options.config;
    this.appId = options.appId;
    this.baseUrl = options.baseUrl || 'https://api.fiberwise.ai/api/v1';
    this.credentialsBasePath = '/credentials';
  }

  /**
   * Get the app ID for this instance
   * @returns {string} - App ID from options or config
   */
  getAppId() {
    return this.appId || (this.config && this.config.get('appId'));
  }

  /**
   * Get headers for API requests
   * @returns {Object} - Headers object
   */
  getHeaders() {
    const headers = {
      'Content-Type': 'application/json'
    };

    // Add API key if available
    const apiKey = this.config && this.config.get('apiKey');
    if (apiKey) {
      headers['X-API-Key'] = apiKey;
    }

    return headers;
  }

  /**
   * Get the full URL to initiate OAuth authentication for a provider
   * @param {string} providerId - The provider ID to authenticate with
   * @param {Object} options - Additional options
   * @param {string} options.returnTo - Path to return to after authentication
   * @returns {string} - Full URL to initiate authentication
   */
  getAuthUrl(providerId, options = {}) {
    if (!providerId) {
      throw new Error('Provider ID is required');
    }

    const appId = this.getAppId();
    const queryParams = new URLSearchParams();

    // Add return path if available
    if (options.returnTo) {
      queryParams.set('return_to', options.returnTo);
    }

    // Add app ID if available
    if (appId) {
      queryParams.set('app_id', appId);
    }

    // Build the full URL with query parameters
    const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';
    return `${this.baseUrl}${this.credentialsBasePath}/auth/initiate/${providerId}${queryString}`;
  }

  /**
   * Initiate OAuth authentication with a provider (redirects browser)
   * @param {string} providerId - The provider ID to authenticate with
   * @param {Object} options - Additional options
   * @param {string} options.returnTo - Path to return to after authentication
   * @returns {Promise} - Redirects the browser
   */
  initiateAuth(providerId, options = {}) {
    try {
      const authUrl = this.getAuthUrl(providerId, options);
      // Use AppBridge router for navigation instead of direct window.location
      if (window.FIBER?._appBridge?._router) {
        window.FIBER._appBridge._router.navigateTo(authUrl);
      } else {
        console.error('AppBridge router not available for OAuth navigation');
        throw new Error('Router not available for OAuth flow');
      }
      return Promise.resolve();
    } catch (error) {
      return Promise.reject(error);
    }
  }

  /**
   * List all OAuth connections for the current user
   * @param {Object} options - List options
   * @param {string} options.appId - Optional app ID to filter connections
   * @returns {Promise} - List of connections
   */
  async listConnections(options = {}) {
    try {
      // Get app ID from options or from instance
      const appId = options.appId || this.getAppId();

      // Build query parameters
      const queryParams = new URLSearchParams();
      if (appId) {
        queryParams.set('app_id', appId);
      }

      const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';

      // Use the service providers endpoint
      const response = await fetch(
        `${this.baseUrl}/apps/${appId}/service-providers${queryString}`,
        {
          method: 'GET',
          headers: this.getHeaders(),
          credentials: 'include'
        }
      );

      if (!response.ok) {
        throw new Error(`Failed to list connections: ${response.statusText}`);
      }

      const providers = await response.json();

      // Filter to only return actually connected providers
      return (providers || []).filter(provider => Boolean(provider.is_connected));
    } catch (error) {
      console.error('Error listing connections:', error);
      return [];
    }
  }

  /**
   * Revoke an OAuth connection
   * @param {string} connectionId - The connection ID to revoke
   * @param {Object} options - Additional options
   * @param {string} options.appId - Optional app ID context
   * @returns {Promise} - Result of the operation
   */
  async revokeConnection(connectionId, options = {}) {
    if (!connectionId) {
      throw new Error('Connection ID is required');
    }

    try {
      // Get app ID from options or from instance
      const appId = options.appId || this.getAppId();

      // Build query parameters
      const queryParams = new URLSearchParams();
      if (appId) {
        queryParams.set('app_id', appId);
      }

      const queryString = queryParams.toString() ? `?${queryParams.toString()}` : '';

      // Use the OAuth revoke endpoint
      const response = await fetch(
        `${this.baseUrl}/apps/${appId}/oauth/revoke/${connectionId}${queryString}`,
        {
          method: 'POST',
          headers: this.getHeaders(),
          credentials: 'include'
        }
      );

      if (!response.ok) {
        throw new Error(`Failed to revoke connection: ${response.statusText}`);
      }

      return await response.json();
    } catch (error) {
      console.error(`Error revoking connection for ${connectionId}:`, error);
      throw error;
    }
  }

  /**
   * Check if the current page load is the result of a successful OAuth callback
   * @param {string} providerId - Provider ID to check for
   * @returns {boolean} - True if this page load is from a successful connection
   */
  isConnectionCallback(providerId) {
    // Get URL parameters from AppBridge
    let searchParams = '';
    if (window.FIBER?._appBridge?._router?.getSearchParams) {
      searchParams = window.FIBER._appBridge._router.getSearchParams();
    } else {
      console.warn('AppBridge router not available for URL parameter parsing');
      return false;
    }

    const params = new URLSearchParams(searchParams);

    // Check if all required parameters are present
    return (
      params.get('connected') === 'true' &&
      params.get('authenticator') === providerId &&
      params.get('app_id') === this.getAppId()
    );
  }

  /**
   * Check if a provider is connected
   * @param {string} providerId - Provider ID or name to check
   * @param {Object} options - Additional options
   * @param {string} options.appId - Optional app ID context
   * @returns {Promise} - True if connected
   */
  async isConnected(providerId, options = {}) {
    try {
      const connections = await this.listConnections(options);
      return connections.some(connection =>
        connection.authenticator_id === providerId ||
        connection.name === providerId
      );
    } catch (error) {
      console.error(`Error checking connection status for provider ${providerId}:`, error);
      return false;
    }
  }
}

export default CredentialService;

        

Key CredentialService Patterns

  • AppBridge Integration: Uses window.FIBER._appBridge._router for OAuth navigation
  • Secure Credentials: Uses credentials: 'include' for cookie-based auth
  • Return URL Handling: returnTo parameter for post-OAuth redirect
  • Connection Filtering: Filters by is_connected status

Step 4: Building the Connection UI Component

The email-connections component provides a complete UI for managing OAuth connections. Here are the key patterns:

Component Structure

// email-connections.js - Web Component for OAuth connections
import { FIBER } from '../../index.js';
import { emailService } from '../services/email-service.js';
import { dataService } from '../services/data-service.js';

class EmailConnections extends HTMLElement {
  constructor() {
    super();
    this.providers = [];
    this.connections = [];
    this.loading = true;
    this.error = null;
    this.successMessage = null;

    // Create shadow DOM for encapsulation
    this.attachShadow({ mode: 'open' });

    // Initialize the DOM
    this.render();

    // Bind event handlers
    this.handleConnectionStatus = this.handleConnectionStatus.bind(this);
    this.clearMessages = this.clearMessages.bind(this);
  }

  // Lifecycle: Component added to DOM
  connectedCallback() {
    console.log('EMAIL-CONNECTIONS: Component connected');
    this.loadData();
    // Listen for connection changes from credential service
    window.addEventListener('connection-status', this.handleConnectionStatus);
  }

  // Lifecycle: Component removed from DOM
  disconnectedCallback() {
    window.removeEventListener('connection-status', this.handleConnectionStatus);
  }

  // Handle connection status updates from OAuth callback
  handleConnectionStatus(event) {
    const status = event.detail;
    if (status.status === 'connected') {
      this.successMessage = `Successfully connected to ${status.authenticator_name || 'provider'}`;
      this.loadData(); // Refresh the data
      this.render();   // Update the UI
    }
  }
}

Loading Provider Data

// Load providers and connections data
async loadData() {
  console.log('EMAIL-CONNECTIONS: loadData() called');
  this.loading = true;
  this.error = null;
  this.render();

  try {
    // Get available providers for this app
    const providers = await dataService.getServiceProviders();

    // Transform provider data with proper ID mapping
    this.providers = (providers || []).map(provider => ({
      id: provider.provider_id || provider.authenticator_id,
      provider_id: provider.provider_id || provider.authenticator_id,
      authenticator_id: provider.authenticator_id || provider.provider_id,
      provider_key: provider.provider_key,
      name: provider.name || provider.display_name,
      icon: `/static/icons/${(provider.authenticator_type || 'email').toLowerCase()}.svg`,
      description: `Connect your ${provider.name || provider.display_name} account.`,
      scopes: provider.scopes || [],
      type: provider.authenticator_type,
      is_connected: Boolean(provider.is_connected),
      connection_status: provider.connection_status ||
        (provider.is_connected ? 'connected' : 'disconnected')
    }));

    // Filter connected providers from the same list
    this.connections = this.providers.filter(provider => provider.is_connected);

  } catch (error) {
    console.error('Error loading provider data:', error);
    this.error = 'Failed to load provider information. Please try again.';
  } finally {
    this.loading = false;
    this.render();
  }
}

Connecting to a Provider

// Connect to a provider - initiates OAuth flow
async connectAuthenticator(authenticatorId) {
  console.log('EMAIL-CONNECTIONS: connectAuthenticator called with ID:', authenticatorId);
  try {
    // Find the provider to get proper ID
    const provider = this.providers.find(p => p.id === authenticatorId);

    if (!provider) {
      throw new Error('Provider not found');
    }

    // Use the actual authenticator_id from the provider
    const actualId = provider.authenticator_id || provider.provider_id;

    if (!actualId) {
      throw new Error('No valid authenticator ID found for provider');
    }

    // Update the UI immediately to show connecting state
    const providerIndex = this.providers.findIndex(p => p.id === authenticatorId);
    if (providerIndex >= 0 && provider.connection_status !== 'pending') {
      this.providers[providerIndex].connection_status = 'pending';
      this.providers[providerIndex].is_connected = false;
      this.render(); // Re-render to show pending state immediately
    }

    // Determine the return URL for after authentication
    const returnPath = `/email-agent-app/settings/email-connections`;

    // Use the connectAuthenticator method from the FIBER.credentials service
    if (!FIBER || !FIBER.credentials) {
      throw new Error('FIBER SDK not available');
    }

    await FIBER.credentials.connectAuthenticator(actualId, {
      returnTo: returnPath
    });

    // The connectAuthenticator method handles the redirect
  } catch (error) {
    console.error('Error initiating connection:', error);

    // Reset the provider state on error
    const providerIndex = this.providers.findIndex(p => p.id === authenticatorId);
    if (providerIndex >= 0) {
      this.providers[providerIndex].connection_status = 'disconnected';
      this.providers[providerIndex].is_connected = false;
    }

    this.error = `Failed to connect to provider. Please try again: ${error.message}`;
    this.render();
  }
}

Disconnecting a Provider

// Disconnect a provider - revokes OAuth connection
async disconnectConnection(connectionId) {
  console.log('EMAIL-CONNECTIONS: disconnectConnection called with:', connectionId);

  if (!connectionId || connectionId === 'undefined') {
    console.error('EMAIL-CONNECTIONS: Invalid connection ID:', connectionId);
    this.error = 'Invalid connection ID. Please refresh the page and try again.';
    this.render();
    return;
  }

  if (!confirm('Are you sure you want to disconnect this provider? ' +
               'This will remove access to your email account.')) {
    return;
  }

  try {
    await dataService.revokeConnection(connectionId);
    this.successMessage = 'Provider disconnected successfully';
    // Refresh connections
    await this.loadData();
  } catch (error) {
    console.error('Error disconnecting provider:', error);
    this.error = 'Failed to disconnect provider. Please try again.';
    this.render();
  }
}

Testing a Connection

// Test a connection - verify OAuth tokens are still valid
async testConnection(connectionId) {
  try {
    this.loading = true;
    this.render();

    const result = await emailService.checkConnectionStatus(connectionId);

    if (result.status === 'success' && result.result?.connected) {
      this.successMessage = 'Connection verified successfully';
    } else {
      this.error = result.result?.message || 'Connection verification failed';
    }
  } catch (error) {
    console.error('Error testing connection:', error);
    this.error = 'Failed to test connection. Please try again.';
  } finally {
    this.loading = false;
    this.render();
  }
}

// Register the custom element
customElements.define('email-connections', EmailConnections);

UI State Management Patterns

  • Optimistic UI: Show "pending" state immediately on connect
  • Event-driven updates: Listen for 'connection-status' events
  • Error recovery: Reset state on connection failure
  • Confirmation dialogs: Confirm before destructive operations

Step 5: Using OAuth in Python Agents

The EmailAgent demonstrates the proper pattern for using OAuth credentials in FiberWise agents. Here's the real implementation:

Agent Class Structure

"""
Generic email agent that uses OAuth credentials to access email services
across different providers (Google, Microsoft, Yahoo)

This agent demonstrates the FiberWise agent development pattern with dependency injection.
It handles email operations across multiple providers while following the platform spec.

Architecture:
- Inherits from FiberAgent for proper SDK integration
- Uses execute method for ia_modules compatibility
- Leverages dependency injection for FiberApp and services via kwargs
- Implements comprehensive email operations across providers
"""

import asyncio
import base64
import json
import logging
from datetime import datetime, timedelta
from typing import Dict, Any, List, Optional, Union, Literal
from enum import Enum
from pydantic import BaseModel, Field, validator, model_validator

from fiberwise_sdk import FiberApp, FiberAgent
from fiberwise_sdk.llm_provider_service import LLMProviderService
from fiberwise_sdk.credential_agent_service import BaseCredentialService

# Set up logger
logger = logging.getLogger(__name__)


class OperationType(str, Enum):
    """Supported email operations"""
    SEARCH_EMAILS = "search_emails"
    GET_EMAIL = "get_email"
    CREATE_DRAFT = "create_draft"
    SEND_EMAIL = "send_email"
    ANALYZE_EMAIL = "analyze_email"
    UPDATE_LABELS = "update_labels"
    LIST_LABELS = "list_labels"
    CHECK_CONNECTION = "check_connection"
    GET_ACCOUNT_STATS = "get_account_stats"
    GET_INBOX = "get_inbox"

The execute() Pattern with Dependency Injection

class EmailAgent(FiberAgent):
    """
    Agent for working with emails across different providers using OAuth.

    Dependency Injection:
    - fiber: FiberApp SDK instance for platform API access
    - llm_service: LLMProviderService for AI-powered email analysis
    - oauth_service: BaseCredentialService for secure OAuth operations

    Execution Pattern:
    The activation processor detects this class inherits from FiberAgent and calls
    the execute method with proper dependency injection via kwargs.
    """

    # Provider-specific endpoint mappings
    PROVIDER_ENDPOINTS = {
        "google": {
            "list_messages": "https://gmail.googleapis.com/gmail/v1/users/me/messages",
            "get_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}",
            "list_labels": "https://gmail.googleapis.com/gmail/v1/users/me/labels",
            "modify_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/{message_id}/modify",
            "send_message": "https://gmail.googleapis.com/gmail/v1/users/me/messages/send",
            "drafts": "https://gmail.googleapis.com/gmail/v1/users/me/drafts"
        },
        "microsoft": {
            "list_messages": "https://graph.microsoft.com/v1.0/me/messages",
            "get_message": "https://graph.microsoft.com/v1.0/me/messages/{message_id}",
            "list_labels": "https://graph.microsoft.com/v1.0/me/mailFolders",
            "modify_message": "https://graph.microsoft.com/v1.0/me/messages/{message_id}",
            "send_message": "https://graph.microsoft.com/v1.0/me/sendMail",
            "drafts": "https://graph.microsoft.com/v1.0/me/mailFolders/drafts/messages"
        },
        "yahoo": {
            "list_messages": "https://mail.yahooapis.com/v1/users/me/messages",
            "get_message": "https://mail.yahooapis.com/v1/users/me/messages/{message_id}",
            "list_labels": "https://mail.yahooapis.com/v1/users/me/folders",
            "modify_message": "https://mail.yahooapis.com/v1/users/me/messages/{message_id}",
            "send_message": "https://mail.yahooapis.com/v1/users/me/messages",
            "drafts": "https://mail.yahooapis.com/v1/users/me/drafts"
        }
    }

    async def execute(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
        """
        Process input data to perform email operations using credential service.

        Args:
            input_data: The input data for the agent
            **kwargs: Injected services (fiber, llm_service, oauth_service, etc.)

        Returns:
            Processing results

        Note:
            Services are injected via kwargs or from self._injected_services
        """
        # Access injected services from kwargs or self
        fiber = kwargs.get('fiber') or (self.fiber if hasattr(self, 'fiber') else None)
        llm_service = kwargs.get('llm_service') or (self.llm_service if hasattr(self, 'llm_service') else None)
        oauth_service = kwargs.get('oauth_service') or (self.oauth_service if hasattr(self, 'oauth_service') else None)

        logger.info(f"Processing email operation: {input_data}")

        # Extract operation from input data
        operation = input_data.get("operation", "search_emails")

        # Get connection_id (supports both naming conventions)
        connection_id = input_data.get("connection_id") or input_data.get("authenticator_id")

        if not connection_id:
            return {
                "status": "error",
                "message": "connection_id or authenticator_id is required"
            }

        # Generate a unique task ID for this operation
        task_id = f"email_agent_{operation}_{datetime.now().strftime('%Y%m%d%H%M%S')}_{id(input_data)}"

        # Send initial update - operation started
        await self.send_agent_update(
            fiber=fiber,
            task_id=task_id,
            status="started",
            progress=0.1,
            message=f"Starting email operation: {operation}",
            provider_id=connection_id,
            operation=operation
        )

        try:
            # Get provider info to determine the provider type
            provider_info = await oauth_service.get_provider_info(connection_id)

            # Check if the OAuth service returned an error
            if not provider_info.get("success", True) or "error" in provider_info:
                error_msg = provider_info.get("error", "Unknown error from OAuth service")
                logger.error(f"OAuth service error for authenticator {connection_id}: {error_msg}")

                return {
                    "status": "error",
                    "message": f"OAuth service error: {error_msg}"
                }

            # Detect provider type from authenticator data
            inner_provider_info = provider_info.get('provider_info', provider_info)
            provider_type = self._detect_provider_type_from_authenticator(inner_provider_info)

            if provider_type not in self.PROVIDER_ENDPOINTS:
                return {
                    "status": "error",
                    "message": f"Unsupported provider type: {provider_type}"
                }

            # Route to appropriate operation handler
            if operation == "search_emails":
                result = await self.search_emails(
                    cred_service=oauth_service,
                    connection_id=connection_id,
                    provider_type=provider_type,
                    query=input_data.get("query", ""),
                    max_results=input_data.get("max_results", 10),
                    label=input_data.get("label"),
                    days_back=input_data.get("days_back", 30),
                    fiber=fiber
                )
            elif operation == "analyze_email":
                result = await self.analyze_email(
                    fiber=fiber,
                    cred_service=oauth_service,
                    llm_service=llm_service,
                    connection_id=connection_id,
                    provider_type=provider_type,
                    message_id=input_data.get("message_id"),
                    user_id=input_data.get("user_id"),
                    app_id=input_data.get("app_id")
                )
            # ... other operations

            # Send completion update
            await self.send_agent_update(
                fiber=fiber,
                task_id=task_id,
                status="completed",
                progress=1.0,
                message=f"Operation {operation} completed successfully",
                provider_id=connection_id,
                operation=operation
            )

            return {
                "status": "success",
                "connection_id": connection_id,
                "provider_type": provider_type,
                "operation": operation,
                "result": result,
                "timestamp": datetime.now().isoformat(),
                "task_id": task_id
            }

        except Exception as e:
            # Send error update
            await self.send_agent_update(
                fiber=fiber,
                task_id=task_id,
                status="failed",
                progress=0,
                message=f"Operation failed: {str(e)}",
                provider_id=connection_id,
                operation=operation
            )

            return {
                "status": "error",
                "message": str(e),
                "connection_id": connection_id,
                "operation": operation
            }

Provider Type Detection

def _detect_provider_type_from_authenticator(self, provider_info: Dict[str, Any]) -> str:
    """
    Detect the provider type (google, microsoft, yahoo) from OAuth authenticator data.

    Args:
        provider_info: OAuth authenticator information

    Returns:
        Provider type string ("google", "microsoft", "yahoo") or empty string if unknown
    """
    # Get various fields that might contain provider information
    auth_url = (provider_info.get("auth_url") or
               provider_info.get("authorization_url") or "").lower()

    token_url = (provider_info.get("token_url") or
                provider_info.get("access_token_url") or "").lower()

    authenticator_name = (provider_info.get("authenticator_name") or
                         provider_info.get("name") or "").lower()

    # Combine all fields for comprehensive checking
    all_fields = f"{auth_url} {token_url} {authenticator_name}".lower()

    # Check for Google indicators
    google_indicators = [
        "googleapis.com", "google.com", "accounts.google",
        "oauth2.googleapis", "gmail", "google"
    ]
    if any(indicator in all_fields for indicator in google_indicators):
        logger.info("Detected Google provider")
        return "google"

    # Check for Microsoft indicators
    microsoft_indicators = [
        "microsoftonline.com", "login.microsoftonline", "graph.microsoft",
        "outlook.office365", "microsoft", "outlook", "office365", "azure"
    ]
    if any(indicator in all_fields for indicator in microsoft_indicators):
        logger.info("Detected Microsoft provider")
        return "microsoft"

    # Check for Yahoo indicators
    yahoo_indicators = [
        "yahoo.com", "login.yahoo", "api.login.yahoo", "yahoo"
    ]
    if any(indicator in all_fields for indicator in yahoo_indicators):
        logger.info("Detected Yahoo provider")
        return "yahoo"

    logger.warning(f"Unknown provider type for authenticator")
    return ""

Making Authenticated Requests

async def search_emails(
    self,
    cred_service: BaseCredentialService,
    connection_id: str,
    provider_type: str,
    query: str = "",
    max_results: int = 10,
    label: Optional[str] = None,
    days_back: int = 30,
    fiber = None
) -> Dict[str, Any]:
    """
    Search for emails using the provider's API

    Args:
        cred_service: Credential service for making authenticated requests
        connection_id: Connection ID (OAuth authenticator) to use
        provider_type: Type of provider (google, microsoft, yahoo)
        query: Search query
        max_results: Maximum number of results to return
        label: Filter by label/folder
        days_back: How many days back to search

    Returns:
        Search results
    """
    # Get the appropriate endpoint for this provider
    endpoint = self.PROVIDER_ENDPOINTS[provider_type]["list_messages"]

    # Build provider-specific query parameters
    params = {}
    date_start = datetime.now() - timedelta(days=days_back)

    if provider_type == "google":
        # Gmail API
        search_query = []
        if query:
            search_query.append(query)
        search_query.append(f"after:{date_start.strftime('%Y/%m/%d')}")
        if label:
            params["labelIds"] = [label]
        params["q"] = " ".join(search_query)
        params["maxResults"] = max_results

    elif provider_type == "microsoft":
        # Microsoft Graph API
        search_query = []
        if query:
            search_query.append(f"contains(subject,'{query}')")
        search_query.append(f"receivedDateTime ge {date_start.isoformat()}Z")
        if search_query:
            params["$filter"] = " and ".join(search_query)
        params["$top"] = max_results
        params["$orderby"] = "receivedDateTime desc"
        if label:
            endpoint = f"https://graph.microsoft.com/v1.0/me/mailFolders/{label}/messages"

    # Make the authenticated request using the credential service
    result = await cred_service.make_authenticated_request(
        credential_id=connection_id,
        url=endpoint,
        method="GET",
        params=params
    )

    # Process and standardize the response format
    if result.get("success", False):
        messages = []

        if provider_type == "google":
            raw_messages = result.get("data", {}).get("messages", [])
            messages = [
                {
                    "id": msg.get("id"),
                    "thread_id": msg.get("threadId"),
                    "snippet": msg.get("snippet", "")
                }
                for msg in raw_messages
            ]

        elif provider_type == "microsoft":
            raw_messages = result.get("data", {}).get("value", [])
            messages = [
                {
                    "id": msg.get("id"),
                    "thread_id": msg.get("conversationId"),
                    "subject": msg.get("subject", ""),
                    "sender": msg.get("from", {}).get("emailAddress", {}).get("address", ""),
                    "date": msg.get("receivedDateTime"),
                    "is_read": msg.get("isRead", False),
                    "preview": msg.get("bodyPreview", "")
                }
                for msg in raw_messages
            ]

        return {
            "messages": messages,
            "total_count": len(messages),
            "query": query,
            "max_results": max_results
        }
    else:
        raise Exception(f"Search failed: {result.get('error')}")

Key Agent Patterns

  • Dependency Injection: Services passed via **kwargs
  • Provider Abstraction: Single interface for Gmail, Outlook, Yahoo
  • Authenticated Requests: Use cred_service.make_authenticated_request()
  • Progress Updates: Real-time status via send_agent_update()
  • Error Handling: Comprehensive try/catch with status updates

Step 6: Production Error Handling

Robust error handling is critical for OAuth integrations. Here are the patterns from the real implementation:

Real-time Progress Updates

async def send_agent_update(
    self,
    fiber: FiberApp,
    task_id: str,
    status: str,
    progress: float,
    message: str,
    provider_id: Optional[str] = None,
    message_id: Optional[str] = None,
    operation: Optional[str] = None
) -> bool:
    """
    Send an agent update through the realtime system

    Args:
        fiber: FiberWise SDK instance
        task_id: Unique ID for this task/operation
        status: Current status (started, processing, completed, failed)
        progress: Progress value between 0.0 and 1.0
        message: Human-readable status message
        provider_id: Optional provider ID for context
        message_id: Optional email message ID for context
        operation: Optional operation type

    Returns:
        bool: True if successful, False otherwise
    """
    try:
        # Ensure we have a realtime connection
        if not await self.ensure_realtime_connected(fiber):
            return False

        # Create the update payload
        update_data = {
            "type": "task_progress",
            "task_id": task_id,
            "status": status,
            "progress": progress,
            "message": message,
            "agent_id": "email_agent",
            "timestamp": datetime.now().isoformat()
        }

        # Add optional context info if provided
        if provider_id:
            update_data["service_provider_id"] = provider_id
        if message_id:
            update_data["message_id"] = message_id
        if operation:
            update_data["operation"] = operation

        # Send the update
        await fiber.realtime.send("agent_update", update_data)

        # For key progress points, also send user notifications
        if status == "completed":
            await self.send_notification(
                fiber=fiber,
                type="success",
                title="Operation Completed",
                message=message,
                level="success"
            )
        elif status == "failed":
            await self.send_notification(
                fiber=fiber,
                type="error",
                title="Operation Failed",
                message=message,
                level="error"
            )

        return True
    except Exception as e:
        logger.error(f"[EmailAgent] Error sending agent update: {str(e)}")
        return False

Graceful Degradation

async def ensure_realtime_connected(self, fiber: FiberApp) -> bool:
    """
    Ensure the realtime connection is established.
    Agents can still function without realtime - it's not a fatal error.
    """
    if not hasattr(self, 'realtime_connected'):
        self.realtime_connected = False

    if not self.realtime_connected:
        try:
            await fiber.realtime.connect()
            self.realtime_connected = True
            logger.info("[EmailAgent] Realtime connection established")
            return True
        except Exception as e:
            # Don't treat this as fatal - agents can work without realtime
            logger.warning(f"[EmailAgent] Realtime connection not available: {str(e)}")
            self.realtime_connected = False
            return False
    return True

async def send_notification(
    self,
    fiber: FiberApp,
    type: str,
    title: str,
    message: str,
    level: str = "info",
    data: Dict[str, Any] = None
) -> bool:
    """Send a user-friendly notification with fallback to logging"""
    try:
        # Try realtime, but don't fail if unavailable
        realtime_available = await self.ensure_realtime_connected(fiber)

        if not realtime_available:
            # Log the notification instead of sending via realtime
            logger.info(f"[EmailAgent] Notification (realtime unavailable): {title} - {message}")
            return True

        # Send via realtime
        await fiber.realtime.send("notification", {
            "type": "user_notification",
            "notification_type": type,
            "title": title,
            "message": message,
            "level": level,
            "agent_id": "email_agent",
            "timestamp": datetime.now().isoformat(),
            **({"data": data} if data else {})
        })
        return True
    except Exception as e:
        # Log as fallback
        logger.info(f"[EmailAgent] Notification (fallback): {title} - {message}")
        return False

Error Handling Patterns Summary

Progress Tracking

Send updates at key progress points: started, processing, completed, failed

Graceful Degradation

Realtime unavailable? Log instead. Don't let optional features block core functionality.

Comprehensive Context

Include provider_id, operation, task_id in all error responses for debugging

User Notifications

Send human-readable notifications on completion and failure

Step 7: Multi-Provider OAuth Support

The email agent supports Gmail, Outlook, and Yahoo through a unified abstraction. Here's how to configure multiple providers:

Manifest Configuration for Multiple Providers

# app_manifest.yaml - Multiple OAuth authenticators
oauth:
  authenticators:
    # Gmail (Google)
    - name: Gmail
      type: oauth2
      file: .fiber/local/oauth/google_credentials.json
      scopes:
        - openid
        - profile
        - email
        - https://www.googleapis.com/auth/gmail.readonly
        - https://www.googleapis.com/auth/gmail.send
      additional_params:
        access_type: offline
        prompt: consent

    # Microsoft Outlook
    - name: Outlook
      type: oauth2
      file: .fiber/local/oauth/microsoft_credentials.json
      scopes:
        - openid
        - profile
        - email
        - Mail.Read
        - Mail.Send
        - User.Read
      additional_params:
        response_mode: query

    # Yahoo Mail
    - name: Yahoo
      type: oauth2
      file: .fiber/local/oauth/yahoo_credentials.json
      scopes:
        - mail-r
        - mail-w
        - profile

Provider-Specific API Handling

# Different providers return data in different formats
# The agent normalizes responses for consistent handling

if provider_type == "google":
    # Gmail returns message IDs that need to be fetched individually
    raw_messages = result.get("data", {}).get("messages", [])
    messages = [
        {
            "id": msg.get("id"),
            "thread_id": msg.get("threadId"),
            "snippet": msg.get("snippet", "")
        }
        for msg in raw_messages
    ]

elif provider_type == "microsoft":
    # Microsoft Graph API returns full message details
    raw_messages = result.get("data", {}).get("value", [])
    messages = [
        {
            "id": msg.get("id"),
            "thread_id": msg.get("conversationId"),
            "subject": msg.get("subject", ""),
            "sender": msg.get("from", {}).get("emailAddress", {}).get("address", ""),
            "date": msg.get("receivedDateTime"),
            "is_read": msg.get("isRead", False),
            "preview": msg.get("bodyPreview", "")
        }
        for msg in raw_messages
    ]

elif provider_type == "yahoo":
    # Yahoo Mail API format
    raw_messages = result.get("data", {}).get("messages", [])
    messages = [
        {
            "id": msg.get("id"),
            "thread_id": msg.get("threadId"),
            "subject": msg.get("subject", ""),
            "sender": msg.get("from", {}).get("email", ""),
            "date": msg.get("receivedDate"),
            "is_read": msg.get("isRead", False)
        }
        for msg in raw_messages
    ]

Provider OAuth Comparison

Provider Auth Endpoint Key Scopes Notes
Gmail accounts.google.com gmail.readonly, gmail.send Requires access_type: offline for refresh tokens
Outlook login.microsoftonline.com Mail.Read, Mail.Send Use response_mode: query for SPA compatibility
Yahoo api.login.yahoo.com mail-r, mail-w Requires developer account approval

Congratulations!

You've learned the complete OAuth integration patterns used in production Fiberwise applications:

OAuth Configuration

  • Multi-provider manifest setup
  • Scope configuration
  • Credential file management
  • Additional OAuth parameters

Frontend Integration

  • CredentialService class
  • Connection UI component
  • OAuth flow initiation
  • Callback handling

Agent Development

  • execute() pattern
  • Dependency injection
  • Authenticated API requests
  • Provider abstraction

Production Patterns

  • Error handling
  • Progress updates
  • Graceful degradation
  • Multi-provider support

Next Steps

Explore the Full Implementation

Clone fiber-apps and study the complete email-agent-app code

cd fiber-apps/email-agent-app
fiber app install

Build Your Own OAuth App

Use these patterns to integrate with Salesforce, Slack, or other OAuth providers

OAuth Reference

Advanced Agent Patterns

Learn about agent workflows, tool use, and multi-agent systems

Agent Guide