Auth & Security

Browser-Approved CLI & MCP Login

OAuth device authorization for short-lived, tenant- and environment-bound CLI/MCP credentials without pasted API keys.

The terminal requests access. The browser shows exactly what will be granted.
Approval
Client, tenant, environment, scopes, and duration
Credential
Short-lived access with a rotating refresh family
Exposure

No broad API key in terminal or model-visible configuration

Ductor uses an OAuth 2.0 device-style flow for interactive CLI and MCP clients.

Enablement

auth:
  enabled: true
  api_key_enabled: true

device_auth:
  enabled: true
  issuer: https://ductor.example.com
  device_ttl: 10m
  access_ttl: 1h
  refresh_ttl: 720h
  poll_interval: 5s
  max_duration: 24h
  allow_http: false

Supply the 32-byte-or-longer pepper through DUCTOR_DEVICE_AUTH_PEPPER; do not put it in a committed config file. HTTPS is required except for explicitly enabled loopback development. Migration 477 registers the public ductor-cli and ductor-mcp clients.

User flow

ductor auth login --issuer https://ductor.example.com

The CLI opens the browser when possible and prints only the verification URL and short human code. The approval page shows requested scope and duration. Read-only is the default. Write, destructive, wildcard, or admin scopes require visible elevated approval, and the approving person must already possess every requested permission.

Consent pathA human-readable grant becomes a bounded credential
  1. Request

    The CLI creates a short-lived device grant and receives a human code.

  2. Review

    The browser displays client, tenant, environment, scopes, and duration.

  3. Approve

    An authenticated user grants only permissions they already possess.

  4. Rotate

    The CLI redeems once, then rotates refresh credentials on every use.

Designed for interactive clients

CLI and MCP gain narrow, expiring access with server-side revocation.

Never a secret-pasting workflow

Human codes cannot redeem credentials, and URLs contain no secrets.

The CLI stores returned credentials in the operating-system user config directory with owner-only permissions. ductor auth status is redacted; ductor auth revoke revokes the server-side refresh family before deleting the local file.

Protocol endpoints

  • POST /oauth/device/authorization
  • POST /oauth/token
  • POST /oauth/revoke
  • GET /oauth/device
  • authenticated POST /oauth/device/verify, /approve, and /deny
  • GET /.well-known/oauth-authorization-server

Protocol POST bodies are form-encoded. Polling returns the standard authorization_pending, slow_down, access_denied, expired_token, and invalid_grant results. An early poll durably increases the interval by five seconds.

Security properties

  • Device and refresh secrets are 256-bit random values; Postgres stores only SHA-256 digests. The human code uses HMAC-SHA-256 with the deployment pepper.
  • A SameSite=Strict, HttpOnly browser binding ties approval to the initiating user and tenant. A stolen human code alone cannot poll or redeem.
  • The URL contains no secret or code. Responses use no-store and no-referrer.
  • Completion inserts the access key and refresh family and consumes the grant in one transaction. Plaintext credentials are returned once.
  • Refresh rotates every use. Reuse of an old token atomically revokes the entire family and current access credential.
  • Client disablement revokes its families immediately. MCP remains request-stateless after login and still authorizes every tool call.

Approved duration is a hard ceiling

Refresh cannot extend consent past the duration shown in the browser. Individual access credentials use the shorter of the configured access TTL and the remaining approved window.