Appearance
No raw OAuth tokens on local host
Note: the equivalent page exists in
docs-devicesat/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 family | Where the redirect lands | Where the token is stored | Crossings |
|---|---|---|---|
| HiveCast account login (Google, GitHub, Enterprise SSO) | https://hivecast.ai/api/auth/callback/<provider> | HiveCast system.auth credentials[] table, principal-scoped | The OIDC id_token is consumed cloud-side. Devices never see it. |
| Per-provider inference OAuth (Anthropic, Codex, OpenAI when interactive) | http://localhost:<port>/callback | Device-local: system.factotum writes <matrix-home>/credentials.json | The 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>/callbackThe 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":
A pairing grant carrying a non-NATS bearer token. If you see
googleAccessToken,anthropicRefreshToken,codexJwt, or anything similar inDeviceSetupExchangeResponse.credentials, that's a violation. Per-provider OAuth belongs in the local Factotum store on the Device, not in the pairing grant.An actor other than
system.factotumreading<matrix-home>/credentials.jsondirectly. If a chat actor, an inference catalog, or anything else opens that file, that's a violation. They go throughcredential.lease/credential.materializeinstead.A pairing flow that "imports" provider tokens from the principal. If
auth.device.exchangeever takesprovider/accessTokeninputs to forward to the Device, that is the wrong direction. Per-Device provider OAuth should be initiated from the Device's own browser viasystem.factotum'scredential.import-detected+credential.importops.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>fromcredential.materialize— never rawapiKeystrings 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
- Credentials model — the four credential layers.
- Threat model — T2 specifically.
- Factotum / secrets service — the enforcement actor.
WORKSTREAMS/matrix-web/USER-FLOWS-AND-AUTH-BOUNDARIES.md— the policy doc.
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 inHostLinkStoreorHostDeviceLinkStore).