Skip to content

Refuse credentials from a newer CLI, and stop refreshing a refused client - #959

Merged
jeremy merged 3 commits into
mainfrom
fix/refuse-unknown-oauth-type
Oct 23, 2026
Merged

jeremy merged 3 commits into
mainfrom
fix/refuse-unknown-oauth-type

Conversation

@jeremy

@jeremy jeremy commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Bug

prepareRefresh's default: branch treated any oauth_type it didn't recognize as Launchpad. When a CLI older than v0.8.0 reads a credential that a newer CLI wrote (bc5), it sends Launchpad's client id and secret to bc3's token endpoint. The endpoint answers invalid_client, and the old CLI sends the same refresh again on every later command. One real user on a stale binary is hitting this about 100 times a day.

Old binaries can't be fixed after the fact. This PR makes sure the same thing can't happen again between this version and whatever type a future CLI writes, and that an invalid_client refusal stops the refreshes instead of repeating them.

Fix

  • Unknown types are refused. Only "" (migrated as before) and launchpad take the Launchpad path. bc5 and legacy bc3 behave as before. Any other type gets an auth error before any request is sent and before the Launchpad token-endpoint migration can fill anything in. The error reads: These credentials were written by a newer basecamp CLI (OAuth type "…") and this version cannot refresh them, with the hint Run: basecamp upgrade (or, to keep this version, log in again with it: basecamp auth login …). The type is %q-quoted, so a damaged store can't put terminal controls into the message. The credential stays in the store untouched, for the newer CLI to use. RefreshRefusal (status and doctor) reports the same error.
  • invalid_client on a refresh is terminal. It is read from the SDK's typed refusal, only on 400 or 401 (RFC 6749 §5.2). A 429 is still handled as a rate-limit hold, and a 5xx that names the client is not a verdict on it. The refusal is stored as a permanent refused hold (Until: 0) on the stored login, using the existing RenewalHold machinery that agent mints already use for invalid_client. It is fingerprinted to the refresh token and the client it was sent as. The error tells the person to log in again. Later commands, in this process or any other, answer from the store and send nothing. The hold doesn't block a fresh login (new refresh token) or a corrected BASECAMP_OAUTH_CLIENT_ID/SECRET (different client); either of those sends the refresh again.
  • rememberRenewalHold now takes the subject the stored credential must still match, because the hold's own fingerprint is no longer always renewalSubject. Existing callers pass hold.Client, so their behavior is unchanged.

Tests

internal/auth/refresh_client_test.go:

  • An unknown type is refused with no network request, with and without a stored token endpoint. The message is quoted and sanitized, the hint names upgrade and login, the credential is left as written, and RefreshRefusal agrees.
  • invalid_client is terminal for both bc5 and launchpad: one request, then repeated AccessToken calls and a second Manager on the same store send nothing. The hold is permanent and reported as permanent, and a new refresh token isn't held to it.
  • The hold covers only the client that was refused: after BASECAMP_OAUTH_CLIENT_ID/SECRET change, the refresh goes out again.
  • A 503 naming invalid_client holds nothing and is retried.
  • invalidClient reads the typed code and status, not the message text.

The tests were checked against the old code: the unknown-type and terminal tests fail without the fix. bin/ci passes on Linux. Locally on macOS, the PTY-dependent interactivity tests (/dev/ptmx) fail the same way with or without this change.


Summary by cubic

Fixes refreshing credentials written by a newer CLI: old versions treated any unknown oauth_type as Launchpad, sending Launchpad's client to the wrong token endpoint and drawing invalid_client on every command. It also stops retrying a refresh that the server refused as invalid_client.

  • Unknown OAuth types are refused before anything else in the credential is examined, so even one without a refresh token is named as newer rather than reported as a missing token; the message points to basecamp upgrade or logging in again, and the credential is left untouched for the newer CLI.
  • A refresh answered with invalid_client on a 400 or 401 is stored as a permanent hold, so later commands answer from the store and send nothing; the refusal promises no resend only when that hold was actually stored. 429s and 5xx keep their existing behavior.
  • The hold is fingerprinted to the refresh token plus the client it was sent as, so a fresh login or a corrected BASECAMP_OAUTH_CLIENT_ID/SECRET sends the refresh again.
  • auth status names "log in again" as the remedy for a login held this way, rather than "log in with a new secret".

Written for commit 9219f45. Summary will update on new commits.

View guided diff Turn on auto-fix

…ient

A credential whose oauth_type this version doesn't know was refreshed as
Launchpad: an older CLI reading a newer login sent Launchpad's client to
that login's token endpoint, which answered invalid_client on every
command, indefinitely. Only "" and "launchpad" take the Launchpad path
now; any other unknown type is refused before anything is sent, with
the upgrade as the remedy.

invalid_client on a refresh is terminal: asking again as the same client
cannot succeed, and each ask is charged to the abuse tracker. The refusal
is held on the stored login with no recheck, for that refresh token and
client, so later commands answer locally and say to log in again. A login
or a changed BASECAMP_OAUTH_CLIENT_ID/SECRET is not held to it.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 19:26
@jeremy
jeremy requested a review from a team as a code owner October 23, 2026 19:26
@github-actions github-actions Bot added tests Tests (unit and e2e) auth OAuth authentication labels Oct 23, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-23T20:00:00.251600Z 9219f45 New commits
🔒 Security Review ✅ Completed 2026-10-23T20:00:57.421624Z 9219f45 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI previously approved these changes Oct 23, 2026

Copilot AI 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.

🟢 Approved

The focused refresh safeguards have targeted regression coverage, and no blocking issues were identified.

No open issues 🎉

What changed in this PR

This PR hardens OAuth refresh handling so unsupported credentials are preserved and a refused OAuth client does not trigger repeated refresh requests.

Changes:

  • Reject unknown OAuth types before sending requests, with guidance to upgrade or log in again.
  • Persist permanent invalid_client holds tied to the refresh token and client, allowing retries when either changes.
  • Add regression tests and update existing hold callers to pass an explicit credential subject.

[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

File Description
internal/​auth/​renewal_hold.go Adds client-specific refresh refusal holds and explicit persistence subjects.
internal/​auth/​renewal_hold_test.go Updates tests for the hold persistence signature.
internal/​auth/​refresh_client_test.go Tests unknown types, permanent refusals, client changes, and transient failures.
internal/​auth/​auth.go Rejects unknown types and handles typed invalid_client refusals.
internal/​auth/​agent.go Adapts agent hold persistence without changing its behavior.

Review effort: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89dc7bedc5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/auth.go
A newer type need not renew by refresh token, and one without was
reported as "No refresh token available" with a login as the remedy,
rather than as a newer credential to upgrade for.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 19:39
Copilot AI dismissed their stale review, a newer Copilot review was requested October 23, 2026 19:39
Copilot AI previously approved these changes Oct 23, 2026

Copilot AI 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.

🟢 Approved

Static review found no blocking defects in the scoped refusal handling, credential preservation, or regression coverage.

No open issues 🎉

Review effort: Balanced

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bffbd21a6d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/auth/renewal_hold.go Outdated
Comment thread internal/auth/renewal_hold.go Outdated
…emedy

A refused refresh said it would not be sent again even when the hold that
keeps that promise could not be stored. And auth status told a login held
for invalid_client to log in with a new secret, the remedy for an agent's
refused secret, rather than to log in again.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 19:58
Copilot AI dismissed their stale review, a newer Copilot review was requested October 23, 2026 19:58
@github-actions github-actions Bot added the commands CLI command implementations label Oct 23, 2026

Copilot AI 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.

🟢 Approved

Static review found no blocking issues in refusal detection, hold persistence, or recovery behavior; tests were inspected but not executed.

No open issues 🎉

Review effort: Balanced

@jeremy

jeremy commented Oct 23, 2026

Copy link
Copy Markdown
Member Author

Review threads: 3 resolved (3 fixed, 0 declined).
Nothing open on 9219f45.

@jeremy
jeremy merged commit e121fce into main Oct 23, 2026
33 checks passed
@jeremy
jeremy deleted the fix/refuse-unknown-oauth-type branch October 23, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth OAuth authentication commands CLI command implementations tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants