Skip to content

Principal identity

The protocol's single most important security rule:

Source: CLAUDE.md Rule 5. Quoted: "Identity from transport metadata, NEVER from payload. Transport metadata = authenticated identity. Payload = untrusted. Never trust payload.principalId."

If your op handler reads payload.principalId and authorizes on that value, you have a security bug. The principal is the one the transport authenticated, not whatever the caller put in their JSON.

Why this matters

Anyone with a NATS connection (or a browser tab in a logged-in session) can publish anything to a permitted subject. JSON payload is just bytes; the publisher controls every field. The bytes that the publisher cannot forge are the connection-level credentials NATS used to authenticate.

The session token (cookie, JWT) and the NATS user that the connection authenticates as together identify the principal. Both pass through the transport layer; neither is in the message body.

How transport metadata reaches the actor

A request envelope carries an optional from field. This is informational only — it identifies the sender's mount path, not their authenticated identity. Authentication metadata reaches the actor through:

  1. NATS connection identity. When the gateway accepts a WebSocket connection, it authenticates the user (cookie, OIDC token, host-link credentials). NATS records this as the connect user.
  2. Subject ACL. NATS rejects publishes to subjects outside the user's permitted set. By the time a message lands at an actor's $inbox, NATS has confirmed the publisher had the right to put it there.
  3. MessageContext. Actor handlers receive a context object that carries the authenticated principal id. (The exact API surface is per-runtime; in the browser-side path, principal info comes from system.auth session validation.)

What identity LOOKS like

ConceptFieldExample
Principal idprincipalIdpri_b3f8412e
Account emailemailrichard-santomauro@nimbletec.com
Display namedisplayNameRichard Santomauro
Authority rootauthorityRootCOM.NIMBLETEC.RICHARD-SANTOMAURO (legacy) or space.spc-9c3a417e (Space-rooted)
Linked Device idhostIdhost_abc123
Workspace realmworkspaceRealmnimbletec.engineering

system.auth is the authority for this stack. The session validate op:

op:    auth.session.validate
payload: { token: '<bearer>' }
reply:   { ok: true, principal: { id, email, displayName, ... },
                     authorityRoot, spaceId, hostLinkId }

The HTTP gateway calls this on every request to derive the principal, then attaches the result to the request context for downstream actors.

What payload.principalId is for

It is allowed as informational when both sender and receiver agree the value is documentation, not authorization. Examples:

  • Audit-log payloads where principalId records WHO took an action, surfaced from the trusted context. The actor should still cross-check against the authenticated principal before logging.
  • Multi-step workflows that pass a principal through a payload chain (e.g., "principal X requested approval; please notify principal Y"). The dispatching actor can include principal info; the receiving actor should re-verify against transport metadata.

In every case, the actor MUST authorize against transport metadata, not against payload-claimed identity.

Cross-tenant invariant

Source: MATRIX-AUTHORITY-MODEL.md. Quoted: "No identity may read another principal's data, except through an explicit cross-tenant authority grant (today: none in product)."

A request that would return another principal's data must be rejected unless the calling principal has an explicit grant. Today, no such grants exist in product code; the rule is enforced by:

  • NATS account ACL (subject-prefix isolation per authority root).
  • Actor-level checks (if (principalId !== request.targetPrincipal) reject).
  • HTTP gateway scoping (/api/identity/runtime-summary returns only the authenticated principal's records).

Browser-side specifics

The browser connects to NATS via /nats-ws (same-origin). The HTTP gateway upgrades the connection, mints a NATS-scoped JWT for the user, and proxies the WebSocket. The JWT pins the connection to the principal's authority root — the browser cannot publish to subjects outside it.

Any code that reads principalId from JSON inside the browser process is just as untrusted as a server-side handler reading payload identity. Verify against the session.

Anti-patterns

  • if (payload.principalId === currentUser) authorize(). Trivially bypassed.
  • Logging payload.principalId as the actor. The audit log records fiction.
  • Treating cookie content as the source of truth without validation. The cookie is a bearer reference; validate via auth.session.validate before trusting its claims.
  • Caching principal id across requests without re-validation. Sessions can be revoked; cached identity must re-validate.

See also