Skip to main content

Pairing and Auth

Otto uses two separate trust flows: a node pairing flow that establishes a trusted relationship between the extension node and the relay, and a controller client flow for long-lived controller identities. Both flows end in access/refresh token pairs that authenticate WebSocket sessions.

Source of truth

ConcernPath
Pairing, token, refresh, and revoke endpointspackages/relay/src/index.ts
CLI auth, pair, revoke, and client commandspackages/cli/src/index.ts
Extension pairing poll and token hydrationextension/src/runtime/background-bootstrap.ts

Node pairing flow

Pairing establishes a trusted node-controller relationship without sharing raw credentials. The node requests a challenge, relay issues a short approval code, the controller approves that code, and the node polls challenge state until approved token material is available.

In the extension onboarding UI, relay transport is now user-driven: entering a relay URL does not auto-connect. Use Connect to start transport and pairing status refresh, and use Disconnect to close transport without clearing stored node tokens.

StepEndpointNotes
Node requests challengePOST /api/pairing/requestPayload: { nodeId }
Controller inspects pendingGET /api/pairing/pendingOptional visibility step
Controller approves codePOST /api/pairing/approvePayload: { code }
Node checks statusGET /api/pairing/status?challengeId=...On approval, node stores tokens

CLI commands for this flow:

# List pending auth codes from connected nodes
otto authcode

# Approve a code and store controller tokens locally
otto pair 123-456

# Revoke refresh token and clear local controller auth
otto revoke

The CLI auto-attempts access token refresh when relay returns invalid_access_token. Manual re-pair is only needed when the refresh token also fails or has been revoked.

Challenge recovery semantics

The extension handles orphaned or expired challenge state automatically on bootstrap:

  • Orphaned metadatapairingChallengeId exists without pairingCode: clears stale keys and requests a fresh challenge.
  • Expired by local clockpairingExpiresAt <= now: clears stale keys and reissues a challenge immediately.
  • Challenge not found at relayGET /api/pairing/status returns 404: treats as stale and reissues.
  • Transient relay errors5xx responses: retries with bounded backoff, then resets and reissues.
  • expired status from relay — immediately triggers challenge reissue.

This keeps onboarding from getting stuck at "waiting for challenge" after browser or relay restarts.

Controller client flow

Controller clients can register independently of node pairing. This is the recommended model for long-lived controller identities.

PhaseEndpointNotes
Register clientPOST /api/controller/registerReturns one-time clientSecret and stable clientId
Exchange credentialsPOST /api/controller/tokenPayload: { clientId, clientSecret }; returns access/refresh tokens
Grant node accessPOST /api/controller/accessNode bearer token required; node-owned ACL decision
# Register a new controller client
otto client register --name "my-laptop" --description "Primary workstation controller"

# Exchange credentials for tokens
otto client login

# Check current client state and secret resolution source
otto client status

# Remove a specific client from relay
otto client remove --client-id <id>

# Remove all registered clients
otto client remove --all

# Clear local credentials without removing from relay
otto client forget

Secret handling

  • CLI stores client secrets in OS keychain when available (cross-platform via keytar).
  • Fallback environment variable: OTTO_CONTROLLER_CLIENT_SECRET.
  • OTTO_CONTROLLER_CLIENT_SECRET takes precedence over keychain lookup.
  • Relay stores only salted client-secret hashes at rest — never plaintext secrets.

Controller removal behavior

  • POST /api/controller/remove with { clientId } revokes the controller record and immediately removes ACL grants, refresh sessions, and active controller sockets.
  • POST /api/controller/remove-all applies the same semantics to every registered client.
  • Repeating bulk removal after a full purge is idempotent (removedCount: 0).

Newly registered controller clients start with no node grants (least-privilege default). Relay enforces ACL on every node-targeted command and returns acl_missing_node_grant when access is denied.

Test flow self-registration

otto test auto self-registers a controller when no local identity or tokens are present:

  • Default name: otto-tester; description: Auto-registered controller for otto test flows.
  • No interactive prompts when defaults are used.
  • Auto-registered controller is retained by default after the run.
  • Use --cleanup-test-controller to remove it after completion.

WebSocket auth flow

After hello, each client sends an auth frame with { accessToken }. Relay verifies signature and claims (iss, aud, role, optional node binding), then responds with auth_ack containing effective role and scopes.

Unauthenticated clients cannot send command, lock, or subscription frames.

Refresh flow

Refresh can be performed over HTTP or WebSocket:

  • HTTP: POST /api/auth/refresh with { refreshToken } — returns new access token and rotates refresh token.
  • WebSocket: refresh frame with the same token payload.

Relay persists refresh sessions in runtime storage so valid sessions survive relay restarts.

TokenDefault lifetimeConfig env var
Access token15 minutesOTTO_TOKEN_TTL_MINUTES
Refresh token30 daysOTTO_REFRESH_TTL_DAYS

Revoke flow

POST /api/auth/revoke with { refreshToken } returns { revoked: boolean }. After revocation, that refresh token can no longer mint access tokens. otto revoke wraps this and also clears local controller credentials.

Claims and rotation

Access tokens include issuer, audience, role, identity bindings, and action scopes. Relay validates issuer and audience on every verification. Secret rotation uses dual-key verification with OTTO_TOKEN_SECRET and optional OTTO_TOKEN_PREVIOUS_SECRET for controlled key rollover without immediate session invalidation.

note

Relay auth controls API access. Command requiresAuth controls website session prerequisites. These are independent — a valid relay token does not mean the extension is logged into the target website.

Next steps