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
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
| Value | Visibility | Format / TTL | Purpose |
|---|---|---|---|
| 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)
POST /api/auth/cli-login/init
Content-Type: application/json
{ "device_id": "cli-9f8e7d6c5b4a3210",
"device_name": "macOS - MacBook Pro - dalihamza" }
Response 200:
{
"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)
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:
{
"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)
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)
POST /api/auth/cli-login/token
{ "session_id": "sess-..." }
// 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)
POST /api/auth/cli-login/exchange
{ "session_id": "sess-...", "authorization_code": "..." }
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 900
}
// 410 expired_session ยท 400 invalid_authorization_code
List sessions (dashboard)
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.
{
"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)
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.
| Column | Type | Description |
|---|---|---|
| id | BIGSERIAL | Primary key, used by the revoke endpoint |
| uuid | UUID | Public identifier |
| user_id | BIGINT NULL | Set when the user approves |
| device_id / device_name / ip_address | VARCHAR | From the init request |
| user_code | VARCHAR(9) | Unique, indexed |
| session_id_hash | VARCHAR | SHA-256 of the secret session id |
| authorization_code_hash | VARCHAR | SHA-256, set on approval |
| refresh_token_hash | VARCHAR | SHA-256, set on exchange |
| status | VARCHAR(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.
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
POST /api/auth/cli-login/refresh
Content-Type: application/json
{ "refresh_token": "cli_..." }
Response 200:
{
"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:
| Priority | Source | Example |
|---|---|---|
| 1 (highest) | Project pubspec.yaml โ dart_cloud.dashboard_url | http://localhost:3000 |
| 2 | Backend env CLI_LOGIN_DASHBOARD_URL | https://dashboard.myco.com |
| 3 (default) | Hardcoded default | https://cloud.containerpub.com |
pubspec.yaml (per-project)
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
# .env or docker-compose environment
CLI_LOGIN_DASHBOARD_URL=https://dashboard.mycompany.com
Revocation Model
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).