Skip to content

feat(cli): add pipefy auth login --device#154

Closed
adriannoes wants to merge 1 commit into
refactor/auth-responsesfrom
feat/138-auth-login-device
Closed

feat(cli): add pipefy auth login --device#154
adriannoes wants to merge 1 commit into
refactor/auth-responsesfrom
feat/138-auth-login-device

Conversation

@adriannoes

@adriannoes adriannoes commented May 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for pipefy auth login --device so headless flows (CI runners, SSH-only boxes, dev containers without a system browser) can sign in. Closes #138.

Scope

This PR is now scoped to the device login strategy only. The infra and OAuth response refactors that previously rode along have been carved out into two prerequisite PRs:

Once #257 and #258 land on dev, this PR's base will be re-pointed to dev.

Changes

  • New pipefy_auth/device.py runs the device-code flow end-to-end. CLI prints the user code + verification URL (plus _complete form when the IdP advertises it), then polls per RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only remaining TTL before each request, honor slow_down / authorization_pending.
  • PKCE bound to the device grant. Realms that mandate PKCE on the OIDC client (observed: piporacle staging) reject the device-auth request without code_challenge. Sending it on the auth request + matching code_verifier on the token poll is safe on IdPs that don't enforce it.
  • pipefy auth login --device is the CLI entry point. --device + --no-browser and --device + --callback-timeout <explicit> both hard-error via typer.BadParameter.
  • discovery parses and SSRF-validates device_authorization_endpoint from the OIDC document.
  • docs/cli/auth.md documents the device grant under "Headless / SSH", including the IdP requirement.

Built on the foundations from the prior two PRs: device.py uses TokenResponse / OAuthErrorResponse from #258 and the pipefy_infra.coerce helpers from #257.

Test plan

@adriannoes adriannoes self-assigned this May 21, 2026
@gbrlcustodio gbrlcustodio self-requested a review May 21, 2026 23:05
@gbrlcustodio gbrlcustodio added the enhancement New feature or request label May 21, 2026
@gbrlcustodio gbrlcustodio added this to the User auth milestone May 21, 2026
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from b2200b2 to fb637bb Compare May 29, 2026 22:19
gbrlcustodio added a commit that referenced this pull request May 30, 2026
Address PR #154 review findings by giving the OAuth wire shapes typed
domain objects instead of raw dicts threaded through call sites.

* Add `TokenResponse` (RFC 6749 §5.1) and `OAuthErrorResponse`
  (§5.2 / RFC 8628 §3.5) in a new `responses` module. `TokenResponse`
  replaces `dict[str, object]` across `exchange_code`, `poll_device_token`,
  and `store_session`; `OAuthErrorResponse` replaces three near-duplicate
  `_format_*_error` free functions across `flow.py` and `device.py`.

* `StoredSession` now bundles a `TokenResponse` so the keychain blob
  reads from one source of truth. On-disk JSON keeps the legacy flat
  shape via destructure-on-write / rebuild-on-load, so existing keychain
  entries continue to load without forcing a re-login.

* Move device-authorization-response parsing into
  `DeviceAuthorization.from_payload` and wrap the `expires_in` float
  coercion so a malformed IdP value surfaces as `LoginError` instead of
  a bare `ValueError`.

* Restructure the device poll loop so the first request fires
  immediately. RFC 8628 §3.5 only requires the interval wait *between*
  retryable responses, not before the first poll.

* Hard-error via `typer.BadParameter` when `--device` is combined with
  `--no-browser` or an explicit `--callback-timeout`. Matches the
  conflicting-flag precedent in `portal.py`.

Test surface: 15 new tests (parser edge cases, poll-first timing,
conflicting-flags CLI) plus migration of 394 existing tests to the
new attribute-access shape. Whole workspace passes (2705/2705).
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from 61d3731 to abad06c Compare May 30, 2026 19:55
@gbrlcustodio gbrlcustodio changed the title feat(cli): add pipefy auth login --device (#138) feat(cli): add pipefy auth login --device May 30, 2026
@gbrlcustodio gbrlcustodio changed the base branch from dev to refactor/auth-responses May 30, 2026 19:55
@gbrlcustodio gbrlcustodio force-pushed the refactor/auth-responses branch from f04f589 to 325d6f1 Compare May 30, 2026 20:08
gbrlcustodio added a commit that referenced this pull request May 30, 2026
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from abad06c to 54eeeab Compare May 30, 2026 20:10
@gbrlcustodio gbrlcustodio force-pushed the refactor/auth-responses branch from 325d6f1 to 285500e Compare May 30, 2026 21:35
gbrlcustodio added a commit that referenced this pull request May 30, 2026
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from 54eeeab to 9edc67d Compare May 30, 2026 21:36
@gbrlcustodio gbrlcustodio force-pushed the refactor/auth-responses branch from 285500e to bb19834 Compare May 30, 2026 22:09
gbrlcustodio added a commit that referenced this pull request May 30, 2026
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from 9edc67d to a69487c Compare May 30, 2026 22:09
@gbrlcustodio gbrlcustodio force-pushed the refactor/auth-responses branch from bb19834 to b3ab7c0 Compare May 30, 2026 22:12
gbrlcustodio added a commit that referenced this pull request May 30, 2026
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from a69487c to 129658d Compare May 30, 2026 22:12
@gbrlcustodio gbrlcustodio force-pushed the refactor/auth-responses branch from b3ab7c0 to 01e1cd9 Compare May 30, 2026 22:15
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio gbrlcustodio force-pushed the feat/138-auth-login-device branch from 129658d to dee8399 Compare May 30, 2026 22:15
@gbrlcustodio gbrlcustodio deleted the branch refactor/auth-responses May 31, 2026 23:29
gbrlcustodio added a commit that referenced this pull request May 31, 2026
Implement OAuth 2.0 Device Authorization Grant (RFC 8628) for headless
sign-in flows where the loopback callback dance isn't workable: CI
runners, SSH-only boxes, dev containers without a system browser.

* `pipefy_auth.device` runs the device-code flow end-to-end. The CLI
  prints the user code and verification URL (plus the `_complete` form
  when the IdP advertises it), then polls the token endpoint per
  RFC 8628 §3.5: clamp non-positive intervals to 5s, sleep only the
  remaining TTL before each request, and honor `slow_down` /
  `authorization_pending`.

* PKCE is bound to the device grant. Realms that mandate PKCE on the
  OIDC client (observed: piporacle staging) reject the device-auth
  request without a `code_challenge`; sending it on the authorization
  request and the matching `code_verifier` on the token poll is safe
  on IdPs that don't enforce it.

* `pipefy auth login --device` is the CLI entry point. `--device`
  combined with `--no-browser` or an explicit `--callback-timeout`
  hard-errors via `typer.BadParameter` — the device flow has no
  browser-callback dance to suppress and the deadline is governed by
  the IdP's `expires_in`. The `_DEFAULT_CALLBACK_TIMEOUT_S` constant
  exists so the explicit-default check compares by value.

* `discovery` parses and SSRF-validates `device_authorization_endpoint`
  from the OIDC document; the docstring covers the device path
  alongside the browser dance and token exchange.

* `docs/cli/auth.md` documents the device grant under
  "Headless / SSH", including the IdP requirement
  (`device_authorization_endpoint` in OIDC discovery).

Built on the foundations from the prior two PRs: device.py uses the
shared `TokenResponse` / `OAuthErrorResponse` wrappers from
``pipefy_auth.responses`` and the permissive `pipefy_infra.coerce`
helpers throughout.

Test surface: 27 tests covering the device polling state machine,
TTL-aware sleep, RFC-mandated interval clamp, slow-down handling,
PKCE binding, and the conflicting-flag CLI errors.
@gbrlcustodio

Copy link
Copy Markdown
Collaborator

Replaced by #259 (auto-closed when base branch refactor/auth-responses was deleted on merge of #258). Same commit, rebased onto the new dev.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants