Authentication
Orbital uses passwordless authentication with WebAuthn/passkeys. This page gives a high‑level view of the flow and shows how bootstrap and the CLI fit in. For full browser implementation details, see Passkey Authentication Flow and Passkey Setup Guide.
Passkey flows
Section titled “Passkey flows”Registration (first time)
Section titled “Registration (first time)”sequenceDiagram participant User participant App participant API participant Device
User->>App: Follow invitation link App->>API: POST /auth/passkey/begin?invite={code} API->>App: Challenge + Session ID App->>Device: Create passkey Device->>User: Biometric prompt User->>Device: Authenticate Device->>App: Signed credential App->>API: POST /auth/passkey/complete API->>App: Success + Session tokenLogin (subsequent)
Section titled “Login (subsequent)”sequenceDiagram participant User participant App participant API participant Device
User->>App: Click "Sign in" App->>API: POST /auth/passkey/begin API->>App: Challenge + Session ID App->>Device: Sign challenge Device->>User: Biometric prompt User->>Device: Authenticate Device->>App: Signed response App->>API: POST /auth/passkey/complete API->>App: Success + Auth tokenBrowser implementation (summary)
Section titled “Browser implementation (summary)”Register a new passkey
Section titled “Register a new passkey”import { startRegistration } from '@simplewebauthn/browser';
// Step 1: Begin registrationconst invitationCode = 'abc123xyz';
// Begin registration with invitationconst beginResponse = await fetch(`/api/auth/passkey/begin?invite=${invitationCode}`, { method: 'POST', headers: { 'Content-Type': 'application/json' },});
const { publicKey, sessionId } = await beginResponse.json();
// Step 2: Create credential with deviceconst credential = await startRegistration(publicKey);
// Step 3: Complete registrationconst completeResponse = await fetch('/api/auth/passkey/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'SessionID': sessionId, }, body: JSON.stringify(credential),});
const { token } = await completeResponse.json();// Store token for authenticated requestsAuthenticate with existing passkey
Section titled “Authenticate with existing passkey”import { startAuthentication } from '@simplewebauthn/browser';
// Step 1: Begin loginconst beginResponse = await fetch('/api/auth/passkey/begin', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}),});
const { publicKey, sessionId } = await beginResponse.json();
// Step 2: Sign challenge with deviceconst credential = await startAuthentication(publicKey);
// Step 3: Complete loginconst completeResponse = await fetch('/api/auth/passkey/complete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'SessionID': sessionId, }, body: JSON.stringify(credential),});
// Token is set as httpOnly cookie automaticallyBootstrap Mode (First-Time Setup)
Section titled “Bootstrap Mode (First-Time Setup)”When deploying Orbital for the first time with no admin accounts, the system enters bootstrap mode to allow initial admin registration.
How Bootstrap Works
Section titled “How Bootstrap Works”- Server starts with no admins → creates bootstrap invitation automatically
- Bootstrap invitation logged to console with token
- Invitation endpoint accessible without authentication (only in bootstrap mode)
- First admin registers using invitation token → bootstrap mode disabled
Using the CLI
Section titled “Using the CLI”The recommended way to bootstrap is via the CLI:
# 1. List invitations (no authentication required in bootstrap mode)orbital invite list
# Output:# ● abc123xyz# Status: active# Expires: 2024-12-21T10:30:00Z
# 2. Register with invitation tokenorbital auth register abc123xyz
# Browser opens → complete passkey registration → first admin created
# 3. Authenticate the CLIorbital auth loginUsing curl
Section titled “Using curl”For programmatic access or CI/CD workflows:
# Get bootstrap invitation (requires bootstrap secret header)curl -H "X-Bootstrap-Secret: orbital-bootstrap-d4c8e7b2-9f1a-4b3e-8c6d-5a7e9f2b1c3d" \ https://your-api.com/api/auth/invitations
# Response:# [# {# "token": "abc123xyz",# "createdBy": 0,# "status": "active",# "expiresAt": "2024-12-21T10:30:00Z"# }# ]Then open the registration URL:
https://your-api.com/api/auth/signin?invite=abc123xyzBootstrap Secret
Section titled “Bootstrap Secret”The bootstrap secret header prevents accidental discovery while keeping access simple:
- Header:
X-Bootstrap-Secret - Value:
orbital-bootstrap-d4c8e7b2-9f1a-4b3e-8c6d-5a7e9f2b1c3d(well-known, documented) - Purpose: Light security gate for empty systems
- Works with: CLI (automatic), curl, Postman, scripts
Security Model
Section titled “Security Model”Bootstrap mode is safe because:
- ✅ Empty system - No data exists to protect when adminCount = 0
- ✅ Invitation ≠ access - Still requires passkey registration (phishing-resistant)
- ✅ Self-disabling - Endpoint returns 401 after first admin exists
- ✅ Time-limited - Bootstrap invitations expire in 7 days
- ✅ Audit trail - All bootstrap access attempts are logged
- ✅ Bootstrap secret - Prevents accidental/automated discovery
Deployment Considerations
Section titled “Deployment Considerations”Development/Staging (behind VPN):
# No special configuration neededdocker compose up -d./orbital serveProduction (public internet):
# Same process, bootstrap invitation logged./orbital serve
# Admin uses CLI from secure workstationorbital invite listorbital auth register <token>orbital auth loginFor high-security environments, monitor logs for bootstrap invitation generation and immediately use it to create the first admin.
Next steps
Section titled “Next steps”- Browser‑side integration and API details: Passkey Authentication Flow
- End‑to‑end passkey setup: Passkey Setup Guide
- CLI commands for auth/bootstrap: CLI Reference