Skip to content

OAuth token requests carry no User-Agent, so installs looping on a dead refresh token can't be traced to a version #906

Open

Description

@jeremy

Some basecamp-cli installs keep presenting a refresh token that Basecamp has already refused with invalid_grant. We can't tell which versions they are, because the CLI's token requests carry no User-Agent of their own.

What production shows (2026-10-14)

Over one hour, the token endpoint refused grant_type=refresh_token requests from client_id=basecamp-cli, all with BC5 resource indicators (urn:bc:account:…), in two patterns:

  • One install retries the same dead refresh token every 2 minutes, around the clock.
  • Another sends about 10 refreshes within 3 seconds, about every 15 minutes. That looks like an agent or script starting several commands at once, each refreshing on its own.

Overnight, a handful of installs doing this were the main source of Basecamp's OAuth abuse-tracker blocks, about 950 per half hour at the peak. The server now counts a repeated refused refresh token once (bc3#13781), so it no longer locks these people out. But the requests keep coming, and the people behind them aren't being told to sign in again.

Already fixed in current code

#711 and #854 handle this. On invalid_grant, refreshCredential (internal/auth/auth.go) forgets a BC5 credential, so later commands fail locally with "Your session has expired or was revoked" instead of retrying. So these are almost certainly older installs. Nobody has reported the loop itself, open or closed.

What's missing: a User-Agent on token requests

API requests identify the CLI (version.UserAgent() via basecamp.WithUserAgent in internal/appctx/context.go). Token-endpoint requests don't. Refresh, and presumably the code and device exchanges, go through the SDK's oauth.Exchanger, which sets no User-Agent, so they arrive as Go-http-client/2.0. On the server we can see that basecamp-cli is looping, but not which release, so we can't tell whether a current version regressed or it's only old installs.

Ask:

  1. Send version.UserAgent() on every OAuth request: token, refresh, revocation, device authorization, discovery. Either give oauth.Exchanger a User-Agent option in the SDK, or set it from the http.Client the CLI passes in.
  2. Check that Treat a rejected refresh token as an auth failure and say what to run #711's forget-on-invalid_grant path also covers several commands starting at once. The 10-refreshes-in-3-seconds pattern would show whether parallel commands (Queue for a resilience slot instead of failing parallel invocations #712, Stop concurrent basecamp processes from losing a credential, and let an agent authenticate #720) each send the dead token before any of them forgets it. With the User-Agent fix in place, the logs will say which version that is.

The original Launchpad-type credentials are deliberately not forgotten on invalid_grant (see the comment in refreshCredential). That's fine; these requests are all BC5.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions