Appearance
HiveCast account login
A principal signs in to HiveCast through an OIDC provider (Google today; GitHub and others target state). The cloud system.auth actor is the relying party. After successful sign-in, the cloud issues an mx_session cookie + JWT that the browser then carries on every request.
Components
| Component | Source | Role |
|---|---|---|
GoogleOidcLoginProvider | system-auth/src/google-oidc.ts | OAuth state, code-for-token exchange, ID-token verification |
HostSessionService | system-auth/src/host-auth.ts:196-313 | session JWT mint/validate/revoke |
HostPrincipalStore | system-auth/src/host-auth.ts:315-430 | principal records and external-identity table |
HostAuthStateStore | system-auth/src/host-auth.ts (referenced from index.ts) | persistence layer |
Sign-in flow
text
1. Browser: GET /api/auth/login/google ?returnTo=<url>
2. Server: auth.google.login →
- generates a code verifier + challenge (PKCE)
- generates state, stores { verifier, redirect } in pendingLogin
- returns Google authorize URL
3. Browser redirects to Google.
4. User authenticates with Google, consents.
5. Google redirects back: <hivecast>/api/auth/callback/google?code=...&state=...
6. Server: auth.google.callback →
- takes pendingLogin entry by state
- exchangeCodeForTokens(code, verifier) -> id_token + access_token
- verifyGoogleIdToken(id_token) -> verified claims
- HostPrincipalStore.findOrCreate(issuer, sub, claims) -> principalId
- HostSessionService.createSession(principalId, { email, displayName }) -> JWT
- Set-Cookie: mx_session=<JWT>; HttpOnly; Path=/; SameSite=Lax; [Secure]
- Redirect to returnTo
7. Browser: subsequent requests carry the cookie. The server validates
via HostSessionService.validateFromCookie() on each request.Session JWT shape
ts
// projects/matrix-3/packages/system-auth/src/host-auth.ts:12-21
interface IHostSessionClaims {
sub: string; // principalId
email?: string;
displayName?: string;
iss: string; // configured issuer
iat: number; // unix seconds
exp: number; // iat + 7 days
jti: string; // randomBytes(16).toString('hex')
purpose?: 'bus'; // present for short-lived bus tokens
}Encoded as a compact JWS:
base64url(header) . base64url(payload) . base64url(HMAC-SHA256(header.payload))HostSessionService._hmac() uses Node's createHmac('sha256', this._secret). The secret comes from auth.sessionSecret config; if it is shorter than 32 characters the constructor throws.
Validation
validateSession(token):
- Splits into three parts; rejects if not exactly three.
- Recomputes HMAC over
header.payload; rejects if the signature doesn't match (constant-timesafeBase64Equal). - Decodes the payload; rejects if
iss !== this._issuerorexp < now. - Checks
revokedSessions[]for thejti; rejects if present.
validateFromCookie(cookieHeader) is the convenience wrapper that extracts the mx_session= value and calls validateSession. It returns { principalId, claims } on success.
Bus tokens (short-lived)
In addition to the 7-day session cookie, createBusToken(principalId) mints a 5-minute JWT with purpose: 'bus'. This is used to authenticate NATS WebSocket connections from the browser without sending the long-lived cookie value over WebSocket.
The browser's bus client requests a fresh bus token before each NATS connect, presents it as the NATS auth_token, and the NATS server (via operator/JWT mode) checks it against the principal's account.
Logout / revoke
HostSessionService.revokeSession(jti, principalId, expiresAt) records the JTI in revokedSessions[]. Future calls to validateSession() on the same token return null. The revokedSessions[] entries are pruned once their original exp passes.
auth.session.revoke is the bus op; the gateway calls it as part of POST /api/auth/logout. The same op + auth.cookie.clear produce the clear-cookie HTTP response.
Cookie attributes
ts
// projects/matrix-3/packages/system-auth/src/host-auth.ts:269-281
buildSetCookie(token, expiresAt, secure): string {
const flags = [
`${SESSION_COOKIE_NAME}=${token}`,
'HttpOnly',
'Path=/',
`Expires=${expiresAt.toUTCString()}`,
'SameSite=Lax',
];
if (secure) flags.push('Secure');
return flags.join('; ');
}HttpOnly defends against XSS reading the cookie. SameSite=Lax defends against most CSRF. Secure is required in production over HTTPS. The cookie expires at the same moment the JWT does.
Identity from transport metadata
auth.identity.resolve is the bus op that says "given these HTTP headers, what identity do they prove?" It accepts headers and loopback flags, validates the cookie/Bearer via HostSessionService.validateSession, and returns:
ts
{
authenticated: boolean,
principalId: string | null,
email?: string,
displayName?: string,
claims: <full session claims>,
localClient: boolean, // true if loopback indicator says local
}This is the canonical source of truth for "who is the caller" in HTTP contexts. Other actors should call auth.identity.resolve rather than re-implement.
Issuer
HostSessionService is constructed with an issuer string. This shows up as the JWT iss claim and is verified on every request. Using distinct issuer values per environment (hivecast.ai, hivecast.staging, localhost-dev) prevents tokens from one environment validating against another.
See also
- OAuth providers — provider-by-provider detail.
- Device flow — alternate auth path for headless Hosts.
- Identity model — what
principalIdmeans.
Source:
projects/matrix-3/packages/system-auth/src/host-auth.ts(HostSessionService,HostPrincipalStore);projects/matrix-3/packages/system-auth/src/google-oidc.ts(GoogleOidcLoginProvider,verifyGoogleIdToken).