How to Integrate with OAuth Providers like Google and Microsoft in Thunderbolt

Thunderbolt provides a provider-agnostic OAuth layer that unifies Google and Microsoft authentication through adapter modules, a core engine, and a React hook that handles PKCE, state management, and cross-platform redirects automatically.

The Thunderbird Thunderbolt codebase implements a modular OAuth architecture that abstracts provider differences while preserving platform-specific optimizations for web, mobile, and desktop environments. Whether you are building a Tauri-based desktop client or a browser-based interface, the same useOAuthConnect hook orchestrates the complete flow from authorization to credential storage.

Architecture Overview

Thunderbolt’s OAuth implementation is organized into three distinct layers that separate provider logic from platform concerns:

Layer Responsibility Key Files
Provider-specific adapters Retrieve provider config, build the auth URL, exchange the authorization code for tokens, and fetch user info. src/integrations/google/auth.ts & src/integrations/microsoft/auth.ts
Core OAuth engine Defines common types, delegates to the adapters, and decides which redirect strategy to use (web → redirect, Tauri → loopback/server). src/lib/auth.ts, src/lib/oauth-redirect.ts, src/lib/oauth-loopback.ts
UI hook Provides a React hook (useOAuthConnect) that UI components can call to start a flow, handle callbacks, store credentials, and expose loading / error state. src/hooks/use-oauth-connect.ts

Core OAuth Types and Delegation

The src/lib/auth.ts file establishes the provider contract through TypeScript unions and wrapper functions that delegate to the concrete adapters.

// src/lib/auth.ts
export type OAuthProvider = 'google' | 'microsoft'
export type OAuthConfig = { clientId: string; redirectUri: string; scope: string }
export type OAuthTokens = { 
  access_token: string; 
  refresh_token?: string; 
  expires_in: number; 
  token_type: string 
}

The core library re-exports adapter methods through a single providers registry:

export const getOAuthConfig = async (httpClient, provider) => 
  providers[provider].getOAuthConfig(httpClient)

export const buildAuthUrl = async (httpClient, provider, state, codeChallenge, redirectUri?) =>
  providers[provider].buildAuthUrl(httpClient, state, codeChallenge, redirectUri)

Provider-Specific Adapters

Each OAuth provider implements four standardized functions: getOAuthConfig, buildAuthUrl, exchangeCodeForTokens, and getUserInfo.

Google Integration

The src/integrations/google/auth.ts module handles Google-specific endpoints:

  • Config retrieval – Fetches dynamic configuration from the backend endpoint auth/google/config and merges it with the platform-specific redirect URI determined by getOAuthRedirectUri.
  • Authorization URL – Constructs https://accounts.google.com/o/oauth2/v2/auth with PKCE code challenge parameters.
  • Token exchange – POSTs the authorization code to the backend endpoint auth/google/exchange.
  • UserInfo – Calls the Google user-info endpoint to retrieve profile data.

Microsoft Integration

The src/integrations/microsoft/auth.ts file mirrors the Google structure but targets Microsoft Entra ID (formerly Azure AD) endpoints:

  • Config – Fetches from auth/microsoft/config and uses Microsoft-specific client registration.
  • Authorization URL – Points to https://login.microsoftonline.com/.../oauth2/v2.0/authorize with PKCE.
  • Token exchange – POSTs to auth/microsoft/exchange.
  • UserInfo – Queries the Microsoft Graph API for profile details.

Both adapters expose identical signatures, allowing the core engine to treat them interchangeably.

Redirect Strategies and Platform Detection

The src/lib/oauth-redirect.ts module determines the appropriate callback URL based on the runtime environment:

if (!isTauri())               // Web → same origin callback
  return window.location.origin + '/oauth/callback'

if (isMobile())               // Mobile apps – App/Universal Link
  return 'https://thunderbolt.io/oauth/callback'

return window.location.origin + '/oauth-callback.html'      // Desktop fallback

This logic ensures that web applications receive same-origin redirects, mobile deep links trigger the native app, and desktop clients use a local HTML file or loopback server.

Desktop Loopback Server

For Tauri desktop environments where remote redirects are unreliable, Thunderbolt spins up a local HTTP server via the startOAuthFlowLoopback function in src/lib/oauth-loopback.ts.

The flow proceeds as follows:

  1. Server initialization – Invokes the Rust command start_oauth_server to bind to a random available port:

    const port = await invoke<number>('start_oauth_server')
    const redirectUri = `http://localhost:${port}`
  2. PKCE generation – Creates a codeVerifier and codeChallenge for the authorization request.

  3. Event registration – Sets up a Tauri event listener for "oauth-callback" before opening the browser to prevent race conditions.

  4. Browser launch – Opens the system browser with the authorization URL constructed via buildAuthUrl.

  5. Callback handling – The local server receives the OAuth callback, validates the state parameter, and exchanges the authorization code for tokens using exchangeCodeForTokens.

  6. Resolution – Returns { tokens, userInfo } to the caller or null if the 5-minute timeout expires.

React Integration with useOAuthConnect

The src/hooks/use-oauth-connect.ts hook provides the primary interface for UI components. It abstracts platform detection, state management, and credential persistence.

const { connect, processCallback, isConnecting, error, clearError } = useOAuthConnect({
  connectingKey: 'google',
  onSuccess: () => console.log('Connected'),
})

The hook handles three distinct platform paths:

  • Tauri Desktop – Invokes startOAuthFlowLoopback, then stores credentials via updateSettings on success.
  • Tauri Mobile – Builds the auth URL, persists temporary state to SQLite (oauth_state, oauth_provider), and opens the system browser via openUrl. The deep-link listener later invokes processCallback to validate state and exchange codes.
  • Web – Calls redirectOAuthFlow to navigate the browser to the provider’s consent screen. Upon return to /oauth/callback, the route component calls processCallback with the URL parameters.

The processCallback function (lines 59-85) validates the stored state against the callback parameter, exchanges the authorization code, saves the credentials to the settings table, clears the temporary SQLite entries, and triggers the onSuccess callback.

Storing Credentials

Upon successful authentication, saveCredentials writes a JSON blob to the SQLite settings table:

await updateSettings(db, {
  [`integrations_${provider}_credentials`]: JSON.stringify(credentials),
  [`integrations_${provider}_is_enabled`]: 'true',
})

The stored object contains the access token, optional refresh token, expiry timestamp, and a minimal profile including email, name, and picture.

Summary

  • Thunderbolt uses a three-layer architecture: provider adapters for Google/Microsoft, a core engine for platform abstraction, and a React hook for UI integration.
  • Provider adapters in src/integrations/{google,microsoft}/auth.ts implement four standardized functions: getOAuthConfig, buildAuthUrl, exchangeCodeForTokens, and getUserInfo.
  • Platform detection in src/lib/oauth-redirect.ts automatically selects appropriate redirect URIs for web, mobile deep links, or desktop loopback servers.
  • Desktop Tauri clients use src/lib/oauth-loopback.ts to spawn a local HTTP server, eliminating the need for remote callback URLs.
  • The useOAuthConnect hook in src/hooks/use-oauth-connect.ts manages the entire flow including PKCE generation, state validation, token exchange, and credential persistence.

Frequently Asked Questions

How does Thunderbolt handle PKCE for OAuth security?

Thunderbolt generates a PKCE code verifier and challenge within the startOAuthFlowLoopback function in src/lib/oauth-loopback.ts. The challenge is sent to the provider's authorization endpoint, while the verifier is retained locally and sent during the token exchange. This prevents authorization code interception attacks, particularly critical for the desktop loopback server scenario where multiple applications could potentially bind to the same port.

Can I add a new OAuth provider without modifying the core library?

Yes. To add a new provider, create a new adapter module in src/integrations/{provider}/auth.ts that exports the four required functions: getOAuthConfig, buildAuthUrl, exchangeCodeForTokens, and getUserInfo. Then extend the OAuthProvider union type in src/lib/auth.ts to include your new provider string. The useOAuthConnect hook and redirect logic will automatically work with your new provider without additional changes.

What is the difference between web and desktop OAuth flows in Thunderbolt?

The web flow in Thunderbolt uses redirectOAuthFlow to navigate the browser to the provider's consent screen, then handles the callback at /oauth/callback via the processCallback function. In contrast, the desktop Tauri flow uses startOAuthFlowLoopback in src/lib/oauth-loopback.ts to spawn a local HTTP server on a random port, allowing the OAuth provider to redirect to http://localhost:{port}. The desktop flow also uses Tauri event listeners to capture the callback without requiring a remote redirect URL.

How are OAuth credentials stored and secured in Thunderbolt?

Upon successful authentication, saveCredentials in src/hooks/use-oauth-connect.ts stores credentials in the SQLite settings table using the updateSettings function. The credentials are serialized as JSON and stored under keys formatted as integrations_${provider}_credentials and integrations_${provider}_is_enabled. The actual token storage leverages the application's existing SQLite database, which on desktop platforms is encrypted by the host OS keychain or credential manager via Tauri's secure storage plugins, ensuring refresh tokens remain protected at rest.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →