Skip to content

OAuth providers

Different providers behave differently. This page is the reference for each provider's redirect placement, scope set, token storage, and refresh story.

At a glance

ProviderFamilyRedirect landsToken storedRefreshImplemented
GoogleOIDC sign-inHiveCastHiveCast cloudsessionyes (GoogleOidcLoginProvider)
GitHubOIDC sign-inHiveCastHiveCast cloudsessiontarget state
Enterprise SSO (Okta, Entra)OIDC sign-inHiveCastHiveCast cloudsessiontarget state
AnthropicProvider OAuthlocal-loopbackDevice Factotumprovider refresh tokenyes (manual import + paste; full OAuth target state)
Codex (OpenAI)Provider OAuthlocal-loopbackDevice Factotumprovider refresh tokenyes (import from ~/.codex/auth.json)
OpenAI API keyProvider API keyn/aDevice Factotumn/ayes (manual paste)
OllamaLocal servicen/aDevice Factotum (baseUrl only)n/ayes

Google (HiveCast sign-in)

Source: projects/matrix-3/packages/system-auth/src/google-oidc.ts.

Configuration:

ts
interface IGoogleOidcLoginConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;     // https://hivecast.ai/api/auth/callback/google
}

Flow:

  1. auth.google.login builds the authorize URL with PKCE (generateCodeVerifier() + generateCodeChallenge()) and stores { verifier, redirect } in the pending-login store.
  2. Browser → Google → callback to <hivecast>/api/auth/callback/google.
  3. auth.google.callback calls exchangeCodeForTokens(code, verifier) to get id_token + access_token.
  4. verifyGoogleIdToken(id_token) checks signature against Google's JWKS, iss, aud (== configured clientId), exp, email_verified.
  5. HostPrincipalStore.findOrCreate('https://accounts.google.com', sub, claims) returns the principal record (created on first sign-in).
  6. HostSessionService.createSession(principalId, { email, displayName }) mints the cookie.

The Google access_token and refresh_token (if any) are NOT persisted. Only the verified claims map to a principalId. HiveCast does not need to call Google APIs after sign-in.

GitHub / Enterprise SSO (target state)

Same shape as Google: pre-registered redirect at HiveCast, OIDC token verified, principal lookup by (iss, sub). Implementation parallels GoogleOidcLoginProvider. Today only Google is wired.

Anthropic (provider OAuth)

Anthropic OAuth uses local-loopback redirect:

http://localhost:<port>/callback

This is registered with Anthropic per app. Tokens issued belong on the user's machine. Storage:

ts
{
  type: 'oauth',
  accessToken: '<token>',
  refreshToken: '<token>',
  expiresAt: <unix-ms>,
  scopes: [...],
  source: 'oauth' | 'import-claude',
}

Where it lives: <matrix-home>/credentials.json, written by system.factotum's credential.store op. The import-claude source indicates it was imported from ~/.claude/.credentials.json automatically by credential.import-detected + credential.import.

credential.refresh for Anthropic today is a partial implementation (returns the existing token's expiry; full refresh-token round-trip is target state — see source in system-factotum/dist/index.js:479-490).

Codex (OpenAI)

Same model as Anthropic. The Codex CLI writes ~/.codex/auth.json after its own OAuth, and Factotum's CredentialImporter detects:

ts
// projects/matrix-3/packages/system-factotum/dist/index.js:124-138
const codexPath = path.join(this._homeDir, '.codex', 'auth.json');
if (fs.existsSync(codexPath)) {
  const raw = JSON.parse(fs.readFileSync(codexPath, 'utf8'));
  if (raw?.token || raw?.accessToken) {
    detected.push({
      source: 'codex-cli',
      provider: 'codex',
      description: 'Codex CLI OAuth credentials (~/.codex/auth.json)',
      hasToken: true
    });
  }
}

credential.import-detected returns this. credential.import({ source: 'codex-cli', provider: 'codex' }) actually copies it into Factotum's store.

The Codex token is treated as oauth (it has accessToken, refreshToken); the Factotum store handles it the same as Anthropic's.

OpenAI API key

If the user has an OPENAI_API_KEY env var or pastes a key directly, Factotum stores:

ts
{
  type: 'api-key',
  apiKey: '<key>',
  source: 'env' | 'manual',
}

credential.validate for OpenAI hits https://api.openai.com/v1/models with the key as Authorization: Bearer <key>; success means the key is valid.

Ollama

Local-only. Factotum stores:

ts
{
  type: 'local',
  baseUrl: 'http://localhost:11434',
  source: 'env' | 'manual',
}

No tokens. credential.lease returns a lease that credential.materialize converts to { baseUrl: '...', headers: {} }.

Multiple providers, single Factotum

Each Device's Factotum is a single actor with a single per-Device store. Multiple provider credentials co-exist as keys in the credentials.json providers map:

json
{
  "providers": {
    "anthropic": { "type": "oauth", "accessToken": "..." },
    "codex": { "type": "oauth", "accessToken": "..." },
    "openai": { "type": "api-key", "apiKey": "..." },
    "ollama": { "type": "local", "baseUrl": "http://localhost:11434" }
  }
}

credential.list returns metadata for all of them (no secrets).

Cross-Device behaviour

Factotum's store does NOT replicate across Devices. If you pair a second Device, you re-do the per-provider OAuth on that Device. This is intentional — see Why hosts do not receive raw OAuth tokens. A target-state "secret sync" feature would need explicit user opt-in, end-to-end encryption between Devices, and a clear threat model that keeps the cloud out of the encryption key path.

See also

Source: projects/matrix-3/packages/system-auth/src/google-oidc.ts (Google OIDC); projects/matrix-3/packages/system-factotum/dist/index.js (Factotum store, CredentialImporter).