Skip to content

Add Device Authorization Grant (RFC 8628) for headless/container environments - #976

Open
mickume wants to merge 2 commits into
jumpstarter-dev:mainfrom
mickume:device-authorization-grant
Open

Add Device Authorization Grant (RFC 8628) for headless/container environments#976
mickume wants to merge 2 commits into
jumpstarter-dev:mainfrom
mickume:device-authorization-grant

Conversation

@mickume

@mickume mickume commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Add device_authorization_grant() method to the OIDC Config class implementing RFC 8628 Device Authorization Grant flow. This enables login from containerized IDEs (e.g., OpenShift Dev Spaces) where localhost callbacks are unreachable.

Changes:

  • Add JMP_OIDC_DEVICE_FLOW env var constant to env.py
  • Add device_authorization_grant() to Config in oidc.py with full RFC 8628
    compliance: device code request, verification URI display, token polling
    with authorization_pending/slow_down/expired_token/access_denied handling
  • Add --device-flow CLI flag to opt_oidc decorator
  • Add should_use_device_flow() helper for flag + env var detection
  • Wire device flow selection in login() and relogin_client() in login.py
  • Add comprehensive tests for device flow grant, polling, error handling,
    flow selection logic, and env var auto-detection

Replaces #916, which could not be rebased cleanly due to divergence from main (used directly as the PR branch). This PR is rebased onto current upstream main from a dedicated feature branch. Note: the exceptions.py, exceptions_test.py, and shell_test.py changes from #916 are no longer included, as they were already merged upstream separately.

…r environments

Rebased from PR jumpstarter-dev#916 onto current upstream main. Resolves conflicts
caused by upstream advancing past the original PR base; exceptions.py,
exceptions_test.py, and shell_test.py changes were already absorbed
upstream and are no longer part of this diff.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI adds OAuth 2.0 Device Authorization Grant support. Users can enable it with --device-flow or JMP_OIDC_DEVICE_FLOW=1. Login and reauthentication select the device flow and handle provider polling outcomes.

Changes

OIDC device-flow support

Layer / File(s) Summary
Device-flow selection contract
python/packages/jumpstarter/jumpstarter/config/env.py, python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py, python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
Adds the JMP_OIDC_DEVICE_FLOW constant, the --device-flow option, and flag/environment resolution tests.
Device authorization and polling
python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py, python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
Implements provider discovery, device credential requests, token polling, slow_down handling, and grant error handling with asynchronous tests.
Login flow integration
python/packages/jumpstarter-cli/jumpstarter_cli/login.py, python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py
Selects device authorization for login and reauthentication when enabled. Authorization-code flow remains the default. Tests cover both paths and the environment constant.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Config.device_authorization_grant
  participant OIDC discovery
  participant OIDC provider
  CLI->>Config.device_authorization_grant: start device authorization
  Config.device_authorization_grant->>OIDC discovery: read device authorization endpoint
  Config.device_authorization_grant->>OIDC provider: request device credentials
  OIDC provider-->>Config.device_authorization_grant: verification URL and user code
  Config.device_authorization_grant->>OIDC provider: poll token endpoint
  OIDC provider-->>Config.device_authorization_grant: token or grant status
  Config.device_authorization_grant-->>CLI: token data or error
Loading

Possibly related PRs

Suggested reviewers: mangelajo

Poem

I tap the code with paws so light,
The device flow begins tonight.
URLs bloom, tokens hop,
Polling waits, then errors stop.
Login follows the rabbit trail.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: RFC 8628 Device Authorization Grant support for headless and container environments.
Description check ✅ Passed The description directly explains the device-flow implementation, CLI integration, environment variable support, testing, and related changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
python/packages/jumpstarter-cli/jumpstarter_cli/login.py (1)

345-346: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--callback-port is silently ignored when the device flow is selected.

If a user passes both --device-flow and --callback-port, the command ignores --callback-port. Consider a warning so the user knows the port is unused.

♻️ Proposed change
     elif should_use_device_flow(device_flow):
+        if callback_port is not None:
+            click.echo("Warning: --callback-port is ignored when the device authorization grant is used.", err=True)
         tokens = await oidc.device_authorization_grant()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login.py` around lines 345 -
346, Update the device-flow branch around should_use_device_flow and
oidc.device_authorization_grant to warn when --callback-port is provided, since
that option is unused in this flow; preserve the existing device authorization
behavior.
python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py (1)

284-428: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Parametrize the three device-flow selection tests.

The three tests differ only in the CLI flag and the environment variable. The FakeOidcConfig class, the fake fetch_auth_config, and the invocation are duplicated three times. A single parametrized test with a shared fixture reduces the duplication and keeps the cases aligned when the login flow changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py` around lines
284 - 428, Parametrize the three device-flow selection tests into one test
covering CLI flag, environment variable, and no-signal cases. Extract the shared
auth configuration, fake fetch_auth_config, FakeOidcConfig, monkeypatch setup,
and runner invocation into the parametrized test or fixtures, while retaining
assertions that device_authorization_grant is selected only for the flag and
environment-variable cases and authorization_code_grant is selected otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py`:
- Around line 49-51: Update test_flag_takes_priority_over_env to set
JMP_OIDC_DEVICE_FLOW to a non-"1" value instead of deleting it, while keeping
device_flow_flag=True, so the test verifies the explicit flag overrides the
environment setting.

In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py`:
- Around line 252-261: Update the device authorization response handling around
response.json() and device_data["device_code"] to catch non-JSON parsing
failures and missing device_code values, converting both into descriptive
click.ClickException errors. Preserve the existing HTTP-status error handling
and only continue to interval/expires_in processing after valid device data is
obtained.
- Line 1: Replace the asyncio dependency in authentication_code_grant with
anyio: remove the asyncio import, add or reuse the anyio import, and change the
polling delay from asyncio.sleep(interval) to anyio.sleep(interval) while
preserving the existing device-flow loop behavior.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py`:
- Around line 316-330: Update the affected login tests around the device-flow
invocations to accept the tmp_path fixture, construct the nonexistent
client-config path under tmp_path, and store each runner.invoke result. Assert
the result succeeds before checking device_flow_called and auth_code_called,
applying the same changes to the tests at all three referenced invocation
blocks.

---

Nitpick comments:
In `@python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py`:
- Around line 284-428: Parametrize the three device-flow selection tests into
one test covering CLI flag, environment variable, and no-signal cases. Extract
the shared auth configuration, fake fetch_auth_config, FakeOidcConfig,
monkeypatch setup, and runner invocation into the parametrized test or fixtures,
while retaining assertions that device_authorization_grant is selected only for
the flag and environment-variable cases and authorization_code_grant is selected
otherwise.

In `@python/packages/jumpstarter-cli/jumpstarter_cli/login.py`:
- Around line 345-346: Update the device-flow branch around
should_use_device_flow and oidc.device_authorization_grant to warn when
--callback-port is provided, since that option is unused in this flow; preserve
the existing device authorization behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d138bfe8-8c12-4a8b-8b06-29174350edcb

📥 Commits

Reviewing files that changed from the base of the PR and between c18ac85 and e4313dc.

📒 Files selected for processing (5)
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py
  • python/packages/jumpstarter/jumpstarter/config/env.py

Comment thread python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py Outdated
Comment thread python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py Outdated
- oidc.py: use anyio.sleep instead of asyncio.sleep for consistency
  with the rest of the module's async backend
- oidc.py: handle non-JSON device authorization responses and a
  missing device_code field with descriptive ClickExceptions instead
  of raw tracebacks
- oidc_test.py: fix test_flag_takes_priority_over_env to actually
  verify flag precedence over a conflicting env value
- login_test.py: use tmp_path instead of a shared /tmp path, and
  assert the CLI invocation succeeds before checking flow selection

Also fixes a pre-existing bug uncovered by the login_test.py exit-code
assertion: logging in with --client-config pointing at a new file
hardcoded the config's alias to "default" and then tried to mark it
as the current client by that alias, which always failed since the
config was saved to the given path, not the default alias location.
Path-based client configs are no longer treated as alias-addressable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py (1)

261-266: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Validate the JSON value and the device_code value.

If the endpoint returns a valid JSON array, string, or number, Line 262 raises TypeError. This bypasses the ClickException handling. If device_code is empty or not a string, polling starts with an invalid credential.

Validate that device_data is an object and that device_code is a non-empty string before polling.

Proposed fix
-            try:
-                device_code = device_data["device_code"]
-            except KeyError as e:
+            if not isinstance(device_data, dict):
+                raise click.ClickException("Device authorization response must be a JSON object.")
+
+            device_code = device_data.get("device_code")
+            if not isinstance(device_code, str) or not device_code:
                 raise click.ClickException(
                     "Device authorization response is missing the required 'device_code' field."
-                ) from e
+                )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py` around
lines 261 - 266, Update the device authorization response handling around
device_data and device_code to first validate that the decoded JSON value is an
object, then ensure device_code is a non-empty string before polling. Convert
invalid response shapes and values into the existing click.ClickException path,
while preserving the current missing-field error behavior for valid objects
without device_code.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py`:
- Around line 261-266: Update the device authorization response handling around
device_data and device_code to first validate that the decoded JSON value is an
object, then ensure device_code is a non-empty string before polling. Convert
invalid response shapes and values into the existing click.ClickException path,
while preserving the current missing-field error behavior for valid objects
without device_code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ec9c3353-547e-4056-ba21-d7bacde8f9eb

📥 Commits

Reviewing files that changed from the base of the PR and between e4313dc and 3e7a6bb.

📒 Files selected for processing (4)
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • python/packages/jumpstarter-cli/jumpstarter_cli/login_test.py
  • python/packages/jumpstarter-cli/jumpstarter_cli/login.py
  • python/packages/jumpstarter-cli-common/jumpstarter_cli_common/oidc_test.py

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant