Skip to content

Add Device Authorization Grant (pipefy auth login --device) for headless flows #138

Description

@gbrlcustodio

Motivation

The current pipefy auth login (PR #125) requires a working browser on the same host as the CLI:

  1. Bind a loopback HTTP server on 127.0.0.1:<ephemeral>.
  2. Launch the user's browser at the auth URL.
  3. The browser redirects to http://127.0.0.1:<port>/callback — only reachable from the same host.

This breaks for:

  • SSH sessions to remote servers (no local browser; the redirect target is on the remote machine, not the user's laptop).
  • CI runners doing one-time interactive setup.
  • Container shells, locked-down corp laptops with restricted browser access, headless dev environments.

--no-browser (added in PR #125) only changes the launch step — the loopback callback still has to land on the host running the CLI. So it doesn't help.

The OAuth 2.0 Device Authorization Grant (RFC 8628) is the canonical solution: the CLI prints a short code + a URL; the user opens that URL on any device with a browser (laptop, phone), enters the code, signs in; the CLI polls the IdP until authorization completes.

The piporacle Keycloak realm already advertises device_authorization_endpoint in its discovery doc (confirmed in PR #125 smoke test step 0). The pipefy-cli Keycloak client config also has device-flow enabled (per the handoff doc's "Keycloak client config" section).

Proposed command surface

pipefy auth login --device

Behavior:

  1. POST to device_authorization_endpoint (from discovery) with client_id=pipefy-cli + scope=openid profile email offline_access.

  2. Print to the user:

    To sign in:
      1. Open https://signin.pipefy.com/device on any browser.
      2. Enter code: WDJB-MJHT
    
    Waiting for sign-in... (this will time out in 5 minutes)
    
  3. Poll the token endpoint with grant_type=urn:ietf:params:oauth:grant-type:device_code at the interval the IdP specifies (typically 5s).

  4. On success, persist tokens to the keychain via existing storage.store_session — same shape as the loopback flow.

  5. On user denial or timeout, surface a clear error.

Implementation notes

  • New module: oauth/device.py — analogous to oauth/flow.py but for the device grant.
  • Reuses: fetch_provider_metadata, store_session, _http.http_client.
  • No loopback HTTP server, no browser launch — much simpler than the existing flow.
  • Polling loop needs to honor authorization_pending / slow_down errors per RFC 8628 §3.5.

Out of scope

  • Reactive code-entry UI (just print the code/URL; user copies). QR code rendering is a nice-to-have for a future polish PR.
  • Alternative grant types (CIBA, etc.).

Acceptance criteria

  • pipefy auth login --device completes end-to-end against piporacle.
  • User code + verification URL print to stdout in a copyable format.
  • Polling honors the IdP's interval and slow_down signals.
  • On success, the stored session is indistinguishable from a loopback-flow session (same keychain key, same shape).
  • Polling timeout surfaces a clear "Sign-in didn't complete in time. Re-run `pipefy auth login --device` to try again." message.
  • User denial surfaces a clear "Sign-in cancelled by the user." message.
  • Tests cover: happy-path polling, authorization_pending retry, slow_down interval bump, expired_token timeout, access_denied user denial.
  • Docs (docs/cli/auth.md) include a "headless / SSH" section explaining when to use --device.

References

  • RFC 8628 (OAuth 2.0 Device Authorization Grant)
  • PR feat(cli): add pipefy auth login (PKCE-based user sign-in) #125 ("Validated technical facts" section confirms device_authorization_endpoint exists in piporacle discovery; client config has the grant enabled)
  • packages/cli/src/pipefy_cli/oauth/flow.py (the loopback flow this complements)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions