CLI Login via Dashboard

The dart_cloud CLI authenticates without a password using a device-authorization flow (RFC 8628 style): the CLI requests a session, the developer approves it in the authenticated Cloud Panel, and the CLI receives short-lived, revocable tokens. The password never touches the terminal.

โ„น

Every CLI session is visible under Settings โ†’ CLI & Devices in the dashboard and can be revoked at any time. Revocation is enforced on token refresh.

Flow

Flow
dart_cloud login
    โ†“
POST /api/auth/cli-login/init {device_id, device_name}
    โ†“
{session_id (secret), user_code "ABCD-EFGH", verification_url}
    โ†“
Open browser โ†’ /cli-login?user_code=ABCD-EFGH
    โ”œโ†’ Not signed in? Redirect to /login?redirect=... and back
    โ”œโ†’ GET  /api/auth/cli-login/lookup  (shows requesting device)
    โ””โ†’ POST /api/auth/cli-login/approve (Bearer, binds user)
    โ†“
CLI polls POST /api/auth/cli-login/token {session_id}
    โ”œโ†’ {status: "pending"} ...
    โ””โ†’ {status: "approved", authorization_code} (single-use)
    โ†“
POST /api/auth/cli-login/exchange {session_id, authorization_code}
    โ†“
{access_token (JWT, 15 min), refresh_token (30 d)} โ†’ Hive storage

Identifiers and Secrets

ValueVisibilityFormat / TTLPurpose
user_code Terminal + browser URL 8 chars, 24-char alphabet ยท 10 min Human-readable handle; safe to display, never sufficient on its own
session_id CLI only 256-bit random ยท 10 min Secret handle for polling; stored hashed
authorization_code CLI only, once 256-bit random ยท 5 min ยท single-use Exchanged for tokens; stored hashed
refresh_token CLI only 256-bit random ยท 30 days Bound to the session row; burned on revocation
access_token CLI only JWT ยท 15 min Short-lived; revocation takes effect at refresh

API Endpoints

Initiate (CLI)

yaml
POST /api/auth/cli-login/init
Content-Type: application/json

{ "device_id": "cli-9f8e7d6c5b4a3210",
  "device_name": "macOS - MacBook Pro - dalihamza" }

Response 200:

json
{
  "session_id": "sess-...",
  "user_code": "ABCD-EFGH",
  "verification_url": "https://cloud.containerpub.com/cli-login?user_code=ABCD-EFGH",
  "expires_in": 600,
  "interval": 5
}

Lookup pending request (dashboard)

yaml
GET /api/auth/cli-login/lookup?user_code=ABCD-EFGH
Authorization: Bearer <access_token>

Returns the requesting device so the approval card can display it before the user decides:

json
{
  "device_name": "macOS - MacBook Pro - dalihamza",
  "device_id": "cli-9f8e7d6c5b4a3210",
  "created_at": "2026-07-31T10:00:00Z",
  "expires_at": "2026-07-31T10:10:00Z"
}
  • 400 invalid_code โ€” unknown, expired, or already handled code
  • 401 โ€” missing or invalid bearer token

Approve (dashboard)

yaml
POST /api/auth/cli-login/approve
Authorization: Bearer <access_token>

{ "user_code": "ABCD-EFGH" }

Responses:

  • 200 โ€” {"success": true}; session bound to the approving user
  • 400 invalid_code โ€” code not found, expired, or not pending

Poll status (CLI)

yaml
POST /api/auth/cli-login/token

{ "session_id": "sess-..." }
json
// waiting
{ "status": "pending" }

// approved โ€” exchange immediately, the code is single-use
{ "status": "approved", "authorization_code": "..." }

// 410 { "error": "expired_session" }   ยท   403 { "error": "denied" }

Exchange code for tokens (CLI)

yaml
POST /api/auth/cli-login/exchange

{ "session_id": "sess-...", "authorization_code": "..." }
json
{
  "access_token": "...",
  "refresh_token": "...",
  "expires_in": 900
}

// 410 expired_session ยท 400 invalid_authorization_code

List sessions (dashboard)

yaml
GET /api/user/cli-sessions?include_revoked=true
Authorization: Bearer <access_token>

include_revoked=true adds sessions revoked within the last 24 hours (is_active: false) for the "Recently revoked" section. The default response contains active sessions only.

json
{
  "sessions": [
    {
      "id": 12,
      "uuid": "...",
      "device_name": "macOS - MacBook Pro - dalihamza",
      "device_id": "cli-9f8e7d6c5b4a3210",
      "ip_address": "203.0.113.42",
      "created_at": "2026-07-31T10:00:00Z",
      "last_used_at": "2026-07-31T12:30:00Z",
      "is_active": true
    }
  ]
}

Revoke session (dashboard)

yaml
DELETE /api/user/cli-sessions/<id>
Authorization: Bearer <access_token>

// 204 No Content ยท 404 when not owned by the user

Database Schema

Table cli_login_sessions (migration: cli_login_sessions_migration.dart). All secrets are stored hashed (SHA-256) โ€” the backend cannot reconstruct them.

ColumnTypeDescription
idBIGSERIALPrimary key, used by the revoke endpoint
uuidUUIDPublic identifier
user_idBIGINT NULLSet when the user approves
device_id / device_name / ip_addressVARCHARFrom the init request
user_codeVARCHAR(9)Unique, indexed
session_id_hashVARCHARSHA-256 of the secret session id
authorization_code_hashVARCHARSHA-256, set on approval
refresh_token_hashVARCHARSHA-256, set on exchange
statusVARCHAR(20)pending โ†’ approved โ†’ exchanged โ†’ revoked / expired
created_at / expires_at / approved_at / exchanged_at / revoked_at / last_used_at TIMESTAMPTZ Lifecycle audit trail

Dashboard UI

Approval page โ€” /cli-login?user_code=...

Public route in frontend/cloud_panel (Pencil frames VkJIA / LdBAd / bBfJ1). The page reads the user_code query parameter, redirects signed-out users to /login?redirect=... and back, looks up the requesting device, and offers Approve / Deny. States: confirm โ†’ approving โ†’ success, with expired/invalid and error fallbacks.

Session manager โ€” /dashboard/settings/cli-devices

Settings tab "CLI & Devices" (Pencil frame tHg4l): active sessions table (device, IP, last used) with per-row Revoke behind a confirmation dialog, optimistic removal with rollback and an error banner, plus a "Recently revoked" section fed by include_revoked=true (24 h window).

Session Reuse & Refresh

When a user runs dart_cloud login on a device that already has a valid session, the CLI reuses the existing session instead of creating a new one. This avoids duplicate device entries and provides a faster login experience.

Flow
dart_cloud login
    โ†“
Check stored refresh_token in ~/.dart_cloud/hive/
    โ”œโ†’ Found? POST /api/auth/cli-login/refresh {refresh_token}
    โ”‚     โ”œโ†’ 200 โ†’ new access_token saved
    โ”‚     โ”‚       "โœ“ Already authenticated on this device"
    โ”‚     โ””โ†’ 401 session_revoked โ†’ fall through
    โ””โ†’ Not found or failed โ†’ full browser login flow

dart_cloud login -f    (skip refresh check, always full flow)

Refresh endpoint

yaml
POST /api/auth/cli-login/refresh
Content-Type: application/json

{ "refresh_token": "cli_..." }

Response 200:

json
{
  "access_token": "eyJ...",
  "expires_in": 900
}

// 401 { "error": "session_revoked" }

The refresh token itself is not rotated โ€” the same refresh token continues to work until the session is revoked or expires. Only a new access token (JWT, 15 min) is issued.

Dashboard URL Configuration

The verification URL opened in the browser can be configured for local development or self-hosted deployments. The CLI resolves the dashboard URL with the following priority:

PrioritySourceExample
1 (highest) Project pubspec.yaml โ†’ dart_cloud.dashboard_url http://localhost:3000
2Backend env CLI_LOGIN_DASHBOARD_URLhttps://dashboard.myco.com
3 (default)Hardcoded defaulthttps://cloud.containerpub.com

pubspec.yaml (per-project)

yaml
name: my_function

dart_cloud:
  dashboard_url: http://localhost:3000

The CLI replaces the origin of the backend-provided verification URL with this value. Useful when running the Cloud Panel locally with flutter run -d chrome.

Backend environment variable

bash
# .env or docker-compose environment
CLI_LOGIN_DASHBOARD_URL=https://dashboard.mycompany.com

Revocation Model

Flow
User clicks Revoke
    โ†“
DELETE /api/user/cli-sessions/<id>
    โ†“
status = revoked ยท revoked_at = now ยท refresh token burned
    โ†“
CLI next login โ†’ tries POST /api/auth/cli-login/refresh
    โ””โ†’ 401 session_revoked โ†’ falls back to full login flow
  • Access tokens are short-lived JWTs (15 min); revocation is guaranteed at refresh time.
  • Refresh checks the session row; revoked or expired sessions receive 401.
  • Recently revoked sessions stay listed in the dashboard for 24 hours for visibility.

Security Notes

  • No password in the terminal โ€” approval happens in the authenticated browser session.
  • Low-entropy user codes are safe โ€” polling and exchange require the 256-bit secret session_id and the single-use authorization_code.
  • Secrets stored hashed only โ€” a database leak does not enable impersonation.
  • User binding at approval โ€” the CLI cannot choose whose account it accesses.
  • Headless fallback โ€” if no browser can open, the CLI prints the URL and code; approval works from any device.
โš 

Rate limiting on init/polling is tracked as follow-up hardening in the parent plan (.devin/roadmap/planning/cli/cli-login-via-dashboard.html).