Skip to content

No raw OAuth tokens on local host

Note: the equivalent page exists in docs-devices at /apps/docs-devices/pairing/no-raw-oauth-tokens-on-host. This page restates the same architectural rule from the security-domain perspective. Both pages must stay aligned.

This is a real architectural rule, not a polish detail. It is the reason the pairing grant explicitly excludes provider OAuth tokens, and the reason system.factotum (the local credentials actor) is the one component allowed to read or write OAuth credentials on a Device.

The rule (restated)

A pairing grant only contains device-scoped credentials. It MUST NOT contain the principal's OAuth refresh tokens, OAuth access tokens, or OIDC ID tokens for any third-party identity provider.

There are two distinct OAuth families, and they live on opposite sides of the network:

OAuth familyWhere the redirect landsWhere the token is storedCrossings
HiveCast account login (Google, GitHub, Enterprise SSO)https://hivecast.ai/api/auth/callback/<provider>HiveCast system.auth credentials[] table, principal-scopedThe OIDC id_token is consumed cloud-side. Devices never see it.
Per-provider inference OAuth (Anthropic, Codex, OpenAI when interactive)http://localhost:<port>/callbackDevice-local: system.factotum writes <matrix-home>/credentials.jsonThe browser redirect lands on the Device; the token never leaves the Device.

Each line is non-negotiable.

Why each direction

Cloud-side OIDC tokens stay cloud-side

The Google OIDC id_token is verified by HiveCast as the relying party. After the principal record exists, the id_token has served its purpose. Persisting it adds risk without benefit.

If HiveCast ever needs to call Google APIs on the principal's behalf (target state: avatar fetch, calendar widgets, …), it would store the relevant access_token cloud-side in HostCredentialStore. That token is also cloud-only and never reaches the Device.

Device-side provider tokens stay Device-side

Anthropic's OAuth flow expects a loopback redirect:

http://localhost:<port>/callback

The resulting refresh token is meant for the user's machine. Sending it to the cloud:

  • has no benefit — the inference call goes Device→Anthropic directly, not through HiveCast
  • has massive risk — a cloud breach exposes refresh tokens for every user, allowing arbitrary inference calls billed to those users
  • violates the OAuth provider's intent — Anthropic's loopback redirect URI is registered for local-only use; uploading would violate their terms

Same arguments apply to Codex, GitHub PATs entered by the user, and self-hosted Ollama endpoints.

The threat model

What does an attacker get with a principal-level OAuth token?

  • Persistent ability to act AS the principal against any service the principal is logged into via that provider
  • Refresh tokens that survive principal session expiry
  • Cross-product blast radius: a compromised Device could enumerate every service the principal has ever signed into via Google

What does an attacker get with a device-scoped credential?

  • A NATS user JWT scoped to one principal × one Space × one authorityRoot, expiring in 24 hours
  • A heartbeat bearer token for one Host Link, revocable independently
  • Capabilities limited to the scopes[] attached to that link

Per-Device blast radius. Per-Device revocation. The principal's external-provider tokens are unaffected.

Where this is enforced

Cloud side — the redeem response

auth.device.exchange and auth.pair.exchange produce a DeviceSetupExchangeResponse whose credentials map contains only:

  • natsUserJwt (device-scoped)
  • natsUserSeed (device-scoped nkey)
  • hostLinkToken (heartbeat bearer for this link)
  • registryToken (optional, device registry write token)

There is no code path in system-auth that reads from HostCredentialStore.list() (the principal-level credentials store) and writes those values into the exchange response.

Device side — Factotum is the boundary

system.factotum is the one actor on a Device that reads or writes provider credential files. Its op surface is the full op surface. Other actors use credential.lease / credential.materialize.

Four code-review consequences

When reviewing a PR that touches pairing, auth, or credentials, reject the following patterns even if "everything works":

  1. A pairing grant carrying a non-NATS bearer token. If you see googleAccessToken, anthropicRefreshToken, codexJwt, or anything similar in DeviceSetupExchangeResponse.credentials, that's a violation. Per-provider OAuth belongs in the local Factotum store on the Device, not in the pairing grant.

  2. An actor other than system.factotum reading <matrix-home>/credentials.json directly. If a chat actor, an inference catalog, or anything else opens that file, that's a violation. They go through credential.lease / credential.materialize instead.

  3. A pairing flow that "imports" provider tokens from the principal. If auth.device.exchange ever takes provider/accessToken inputs to forward to the Device, that is the wrong direction. Per-Device provider OAuth should be initiated from the Device's own browser via system.factotum's credential.import-detected + credential.import ops.

  4. HTTP headers including raw third-party tokens travelling on the bus. The lease/materialize pattern exists so the bus only ever carries lease IDs. Inference actors that call third-party APIs should only see fully formed headers: Record<string, string> from credential.materialize — never raw apiKey strings on bus payloads.

Mantra

HiveCast credentials live cloud-side. Provider credentials live Device-side. The pairing grant is the bridge — it carries only Device-scoped, Host-Link-bound material in either direction.

If a feature seems to need raw OAuth tokens to cross the boundary, re-design the feature so it doesn't. There has not yet been a case where that re-design wasn't possible.

See also

Source: WORKSTREAMS/matrix-web/USER-FLOWS-AND-AUTH-BOUNDARIES.md § "Auth Boundary Rules"; WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md § "Correct Security Model" and § "Setup-exchange / device redeem response"; projects/matrix-3/packages/system-factotum/ (Factotum op surface); projects/matrix-3/packages/system-auth/src/host-auth.ts (no principal-token paths in HostLinkStore or HostDeviceLinkStore).