Appearance
Authentication
The platform supports three authentication paths today. Each has a distinct lifetime, threat model, and audience.
1. Google OIDC (browser users)
User-facing OIDC flow, present on the platform Host only (not on user-Device installs).
Source: projects/matrix-3/packages/system-auth/src/google-oidc.ts.
Flow
- User clicks "Sign in with Google" on
/apps/web/. auth.google.loginreturns a redirect URL with PKCE state.- Browser navigates to
https://accounts.google.com/o/oauth2/v2/auth?.... - After approval, Google redirects to
/api/auth/callback/google?code=...&state=.... auth.google.callbackexchanges code for tokens, verifies the ID token signature against Google's JWKS, extracts(iss, sub, email, name).principalStore.findOrCreate(iss, sub, ...)returns the principal.sessionService.createSession(principalId, ...)mints anmx_sessionJWT.Set-Cookie: mx_session=...; HttpOnly; Path=/; SameSite=Lax.- Browser is redirected to the original
redirectfrom OAuth state.
Properties
- Token type: HMAC-SHA256-signed JWT. Source:
HostSessionService(host-auth.ts:196-285). - Lifetime:
SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1000— 7 days. - Storage: HttpOnly cookie. Never accessible to JavaScript.
- Verification: signature + issuer + expiry + revocation list.
- Revocation: by
jti(JWT id) intoHostAuthStateStore.revokedSessions[].
Anonymous
If no mx_session cookie is present, auth.identity.resolve returns { authenticated: false }. The browser can still call public endpoints (/healthz, /api/apps).
2. Host-link bearer token (paired Devices)
Each paired Device carries a heartbeat token. Source: rotated by HostLinkStore.rotateHeartbeatToken at exchange and refresh.
Flow
- Pair flow exchange returns
heartbeatTokento the Device. - The Device persists the token (in its own
<host-home>/credentials/). - The Device's
host.controlPOSTs to/_auth/host-link/heartbeatwithAuthorization: Bearer <token>plus a JSON body identifying the link. - The gateway calls
auth.hostLink.verifyHeartbeat. - The op compares an HMAC over the token to
link.heartbeatTokenHash. Constant-time compare. - If valid,
lastRefreshedAtis updated and the response acknowledges.
Properties
- Token type: opaque random bytes, hashed at rest.
- Lifetime: rotates on every credential refresh; explicitly rotates on demand.
- Storage: plaintext on the Device only; hashed on the platform.
- Verification: HMAC compare.
- Revocation: any operation that flips
link.status = 'revoked'invalidates the hash.
3. Local-client (loopback)
When a request arrives over loopback (127.0.0.1 / ::1 / localhost), the gateway treats the connection as local-client.
Source: projects/matrix-3/packages/system-auth/src/index.ts:1311-1320.
ts
if (loopback) {
return {
claims: null,
authenticated: true,
principalId: 'host-service',
displayName: 'Matrix Host Service',
localClient: true,
};
}The local-client identity bypasses session checks. This is how a CLI on the same host invokes auth.principal.list without logging in. It is not what a remote user gets, even if the platform serves on a non-loopback interface — the loopback signal must come from the connection itself.
This is critical: HiveCast must run behind a TLS-terminating proxy (Caddy) that does NOT spoof loopback. If a proxy passes connections through with 127.0.0.1 as the source, the platform mistakenly treats them as local-client. The deploy-cloud Caddy config does not do this; review any new proxy configuration carefully.
NATS user JWT (per-Device, NOT a browser auth path)
In addition to the three above, paired Devices carry a NATS user JWT used for bus traffic only. This is not a session token — it does not authenticate HTTP requests. It authenticates NATS connections, with Ed25519 NKey challenge-response.
The NATS user JWT and the Host-link bearer token serve different layers and are issued by the same exchange but used independently.
Cross-checks
| Check | What it does | Source |
|---|---|---|
iss | Session JWT issued by this Host | HostSessionService.validateSession |
exp | Not expired | same |
| Revocation | Not in revokedSessions[] | same |
| HMAC | Signature valid | same |
| Cookie HttpOnly | JS can't read | gateway sets the flag |
| SameSite=Lax | Mitigates CSRF | gateway sets the flag |
| Origin | (target) Origin header validation on POST | not yet enforced platform-wide |
What is NOT yet implemented
- GitHub OIDC. Schema accepts arbitrary
(issuer, subject)pairs but only Google's flow exists. - Email + passkey. Mentioned in
ARCHITECTURE-PLATFORM-TOPOLOGY.md§3, not implemented. - Enterprise SSO (Okta/Entra/Duo). Schema-ready (
workspaceRealm); flow not built. - Multi-factor (TOTP/WebAuthn) at HiveCast level. The provider's MFA (Google's) is the only factor today.
- Origin header validation on writes. CSRF mitigation today is SameSite=Lax cookies + the fact that NATS bus operations use the Bus token, not the session cookie.
Session secret rotation
HostSessionService requires auth.sessionSecret ≥ 32 chars. To rotate:
- Generate a new secret:
HostSessionService.generateSecret()returns a 32-byte hex string. - Update
host.jsonwith the new secret. - Restart the platform Host.
- All existing sessions become invalid. Users must re-login.
There is no zero-downtime rotation today. Target state.
Live proof commitments
WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md records live proofs:
- Device-scoped credential issuance:
richard-device-scoped-proof-20260501-060747. - Host-link revoke + NATS user JWT revocation:
richard-device-revoke-proof-20260501-062524. - UI revoke:
richard-ui-revoke-proof-20260501-064035. - Credential refresh:
richard-refresh-proof-20260501-065817.
These are the canonical accept-tests for the auth flows on hivecast.ai.
See also
- Authorization — what an authenticated principal is allowed to do.
- Capability tokens — bus tokens, heartbeat tokens.
- Reference: Actor surfaces — full
system.authop list. - Devices / Pairing — Device-side auth.
Source:
projects/matrix-3/packages/system-auth/src/index.ts:435-448forauth.identity.resolve.host-auth.ts:196-285forHostSessionService.google-oidc.tsfor the OIDC flow.