Skip to content

fix: step up OAuth scopes for under-scoped tokens in non-interactive sessions#34

Merged
jpage-godaddy merged 1 commit into
mainfrom
symmetric-step-up
Jun 29, 2026
Merged

fix: step up OAuth scopes for under-scoped tokens in non-interactive sessions#34
jpage-godaddy merged 1 commit into
mainfrom
symmetric-step-up

Conversation

@jpage-godaddy

Copy link
Copy Markdown
Collaborator

Problem

PkceAuthProvider::get_credential_for handled scope acquisition asymmetrically:

  • Token present but under-scoped — gated on session_is_interactive() (a TTY check). With no TTY → hard error: missing required scope(s): …; run auth login --env … --scope … in an interactive terminal.
  • No token at all — ran reauthenticate (browser flow) unconditionally, no interactivity gate.

So a command run from a non-TTY context (agent harness, piped stdio, captured streams) failed when it held a token missing a scope, but auto-authenticated once that token was cleared. This surfaced in a bug bash: gddy domain suggest hard-errored on the first run (token lacked domains.domain:read), the suggested auth login --scope … fix failed (it cleared the token, then requested a scope the OAuth client isn't registered for → no authorization code in callback), and re-running domain suggest then auto-authed and succeeded. The error's advice was the wrong fix path; simply retrying was the answer.

Fix

Drop the interactivity gate from the under-scoped branch so it takes the same reauthenticate(union) path the no-token branch already uses. Missing scopes now always trigger step-up.

  • plan_step_up simplified to Covered / Reauthenticate (no interactive param).
  • Removed the now-unused StepUp::MissingNonInteractive, session_is_interactive, and missing_scope_error (and the IsTerminal import).
  • The requested set is still defaults ∪ granted ∪ required, so step-up never narrows an existing grant; the post-auth ensure_granted check is unchanged.

Accepted tradeoff: a genuinely headless run holding an under-scoped token now opens a browser and times out after 120s (timed out waiting for OAuth callback) instead of failing fast — identical to today's no-token behavior.

Tests

  • Rewrote plan_step_up_covers_or_reauthenticates for the two-outcome signature (missing → Reauthenticate, no interactivity axis).
  • cargo fmt, cargo clippy --all-targets --all-features -D warnings, and full cargo test all clean (134+ tests).

🤖 Generated with Claude Code

…sessions

`get_credential_for` had an asymmetry: when a stored token was missing a
required scope it consulted `session_is_interactive()` and, with no TTY,
returned a hard "missing required scope(s)" error instead of acquiring the
scope. The no-token branch, by contrast, always ran the browser flow. So a
command run from a non-TTY context (e.g. an agent harness or piped stdio)
failed when it held an under-scoped token, but auto-authenticated once the
token was cleared — surprising and inconsistent.

Drop the interactivity gate from the under-scoped branch so it takes the same
`reauthenticate(union)` path the no-token branch already uses. Missing scopes
now always trigger step-up; the browser flow's 120s callback timeout bounds
truly-headless hangs (identical to today's no-token behavior). The union
(defaults ∪ granted ∪ required) is unchanged, so step-up still never narrows
an existing grant.

Simplify `plan_step_up` to Covered/Reauthenticate and remove the now-unused
`MissingNonInteractive` variant, `session_is_interactive`, and
`missing_scope_error`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes inconsistent OAuth scope acquisition behavior in PkceAuthProvider::get_credential_for by making “token present but under-scoped” follow the same reauthentication (browser) flow as the “no token” case, even when stdio is non-interactive.

Changes:

  • Removed the non-interactive “fail fast” branch for under-scoped tokens and always performs step-up via reauthenticate with the union of defaults ∪ granted ∪ required.
  • Simplified plan_step_up to a two-outcome decision (Covered vs Reauthenticate) and deleted the associated TTY detection and error helper.
  • Updated unit tests to reflect the simplified step-up behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jpage-godaddy jpage-godaddy merged commit 9b82ee0 into main Jun 29, 2026
3 checks passed
@jpage-godaddy jpage-godaddy deleted the symmetric-step-up branch June 29, 2026 18:44
jpage-godaddy pushed a commit that referenced this pull request Jun 29, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.4](cli-engine-v0.3.3...cli-engine-v0.3.4)
(2026-06-29)


### Bug Fixes

* change default credential store from Keyring to Auto
([#31](#31))
([ccca021](ccca021))
* step up OAuth scopes for under-scoped tokens in non-interactive
sessions ([#34](#34))
([9b82ee0](9b82ee0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
jpage-godaddy added a commit to godaddy/cli that referenced this pull request Jun 29, 2026
cli-engine 0.3.4 makes OAuth scope step-up fire for under-scoped tokens in
non-interactive sessions (godaddy/cli-engine#34). Before this, a command run
without a TTY (agent harness, piped stdio) that held a token missing a
required scope hard-errored with "missing required scope(s)" instead of
launching the OAuth flow, while the same command auto-authenticated once the
token was cleared.

With the bump, `gddy domain suggest` / `domain purchase` and any scoped
command now step up to acquire the missing scope regardless of interactivity.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.

3 participants