BC5 OAuth: resource-first discovery, device-flow login, RFC 8707 resource echo - #582
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Basecamp CLI authentication to adopt the SDK’s BC5 OAuth stack end-to-end: resource-first discovery (RFC 9728 → 8414), automatic device-flow login when available (RFC 8628), and persistence/echoing of the RFC 8707 resource indicator so multi-account refresh tokens can be refreshed reliably.
Changes:
- Implement provider discovery + BC5 device-flow login path, including endpoint validation and sanitized device-code display, and persist/echo
resourceon refresh. - Update CLI help/docs to be provider-neutral and clarify
--device-codesemantics; document the now-obsoleteclient.json. - Bump
github.com/basecamp/basecamp-sdk/goto the PR’s required pseudo-version + update provenance; expand unit and e2e coverage around auth/flags/refresh behavior.
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.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| skills/basecamp/SKILL.md | Document client.json as obsolete; clarify --scope and --device-code help text. |
| README.md | Update authentication docs to describe device-flow-first behavior and provider-neutral flags; remove client.json from config tree and note it’s safe to delete. |
| internal/version/sdk-provenance.json | Update pinned SDK revision metadata to match the new pseudo-version. |
| internal/sdk/store.go | Add persisted resource field to SDK-facing credentials struct. |
| internal/completion/refresh_test.go | Ensure test client config includes BaseURL for refreshed completion behavior. |
| internal/commands/profile.go | Update profile create flag help strings to match provider-neutral semantics. |
| internal/commands/profile_test.go | Add regression coverage ensuring --device-code forces remote/paste-callback mode. |
| internal/commands/auth.go | Update auth login flag help strings to match provider-neutral semantics. |
| internal/commands/auth_login_test.go | Add regression coverage ensuring --device-code forces remote/paste-callback mode in auth login. |
| internal/auth/keyring.go | Persist resource in stored credentials (keyring/file fallback). |
| internal/auth/device_test.go | Add comprehensive unit coverage for discovery, device flow, endpoint hardening, and RFC 8707 resource echo on refresh. |
| internal/auth/auth.go | Implement resource-first discovery, device-flow login path, refresh resource echo/preservation, and shared endpoint validation. |
| internal/auth/auth_test.go | Update existing auth tests to match the new discovery and Launchpad-only authorization-code behavior. |
| go.mod | Bump basecamp-sdk dependency to required pseudo-version. |
| go.sum | Update module sums for the SDK bump. |
| e2e/auth.bats | Add e2e assertions for updated help text phrasing (provider-neutral flags). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
1 issue found across 16 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="internal/auth/auth.go">
<violation number="1" location="internal/auth/auth.go:762">
P1: Equivalent base-URL spellings with an explicit default port or mixed-case host will miss BC5 discovery and fall back to Launchpad. Canonicalize scheme, hostname, and default ports before returning the resource origin so it matches the protected-resource metadata identifier.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
internal/auth/device_test.go:563
- After making
countingServerreturn a synchronized accessor,hitsis a function; dereferencing it (*hits) won’t compile and also bypasses synchronization. Call the accessor instead.
assert.Zero(t, *hits, "capability failure on a selected issuer must not fall back")
internal/auth/device_test.go:137
countingServerreturns a*intthat is later read without taking the mutex, which will trigger a data race under-racewhen the handler incrementscount. Return a small accessor closure (or an atomic counter) so reads are synchronized too.
// countingServer records how many requests it receives and 404s all of them.
func countingServer(t *testing.T) (*httptest.Server, *int) {
t.Helper()
var mu sync.Mutex
count := 0
Review carefully before merging. Consider a major version bump. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 16 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
internal/auth/auth.go:1053
- AuthorizationEndpoint() now treats stored oauthTypeBC5 the same as bc3 (returns <base_url>/authorization.json), but the AuthorizationEndpoint test suite doesn’t cover the new bc5 branch. Adding a targeted regression test would help prevent future changes from breaking bc5 behavior (and clarify how BASECAMP_TOKEN should be classified when using bc5 tokens).
oauthType := m.GetOAuthType()
switch oauthType {
case "bc3", oauthTypeBC5:
return config.NormalizeBaseURL(m.cfg.BaseURL) + "/authorization.json", nil
case oauthTypeLaunchpad, "":
// "launchpad" = stored credentials; "" = no stored credentials and
Carries the corrective-cycle Go fixes since e9469832: presence-aware refresh expires_in (omission clears the stale expiry, explicit non-positive fails as api_error), the pollToken and post-authorization cancellation re-checks, the empty-device-endpoint capability guard, the no-expiry AccessToken guard, and truncated error interpolation. The pin moves to basecamp-sdk#478's merged SHA before landing.
Carries the final corrective round's Go changes: the refresh expires_in lifetime ceiling (an absurd value overflowed into a negative ExpiresAt read as never-expiring) atop the presence-aware omission/non-positive handling, plus the cancellation re-checks. The pin moves to basecamp-sdk#478's merged SHA before landing.
Also repairs the corrupted updated_at the previous pin wrote (a mis-sliced pseudo-version segment produced a non-ISO timestamp); the timestamp now comes from the pseudo-version's 14-digit datetime field.
DNS hostnames are case-insensitive, and resourceOrigin already lowercases before the same localhost check — but isSecureEndpointURL passed u.Host straight into hostutil.IsLocalhost, so http://LocalHost:3001 was rejected as insecure. Lowercase before matching; the non-loopback rejection is unchanged. The test fails against the un-fixed code.
The README promised a Launchpad fallback whenever device flow was unavailable; discoverOAuth falls back only on the two soft pre-selection outcomes — once a BC5 issuer is selected, failures surface loudly and are never converted into a Launchpad attempt.
requireSecureOAuthEndpoint also rejects userinfo, hostless forms, and out-of-range ports — the message only mentioned the scheme rule, which was misleading for those failures.
resourceOrigin explicitly supports pathful BaseURLs, but AuthorizationEndpoint appended /authorization.json to NormalizeBaseURL — which only trims a trailing slash — yielding a misrouted host/api/v1/authorization.json. Use the canonical origin. The pathful test fails against the un-fixed code.
Absorbs the pre-request issuance anchoring for device login (a slow authorization response now counts against the code lifetime) plus the round's Python/Ruby/TS hardening upstream. No CLI-side code changes.
Absorbs the SPEC §16 receipt-anchoring for device login (request-leg latency no longer shrinks the code window) plus the round's transport and exchange hardening upstream. No CLI-side code changes.
Absorbs the exchange token_type contract (Bearer only when absent, typed api fault on present-but-empty) and the oauth-token fixture tightening upstream. No CLI-side code changes.
Absorbs the duplicate-Retry-After rejection and the round's transport and parse-fault hardening upstream. No CLI-side code changes.
Absorbs the validate-then-mutate refresh ordering and Retry-After digit-bound parity upstream. No CLI-side code changes.
Absorbs the shared Retry-After digit bound in the Go device poller. No CLI-side code changes.
Adopts the SDK's BC5 OAuth stack end to end: resource-first discovery (RFC 9728 + 8414), RFC 8628 device-flow login as the pre-registered public
basecamp-cliclient, and the RFC 8707 multi-account refresh contract.Why the resource echo matters:
basecamp-cliis a trusted client in bc3's registry, so a device approval records an identity-wide grant and mints a multi-account refresh token. bc3's refresh grant hard-requires the RFC 8707resourceparameter for those (400invalid_requestwithout it) — without persisting and echoing it, every device login dies at its first token expiry (~1h). Credentials gain aresourcefield (auth + mirrored sdk store); device login persiststoken.Resource; refresh echoes it and preserves it when the refresh response omits it. An end-to-end test drives login → stored resource → forced refresh → form echo → binding surviving a resource-less rotation.Gating: DRAFT until basecamp-sdk#478 (resource indicator + 429 poll contract, stacked on #376/#370) merges — the SDK pin tracks the #478 branch head (see
go.mod+internal/version/sdk-provenance.jsonfor the current pseudo-version — re-pinned on everygo/change); it moves to the merged SHA before this lands. No SDK release required first, per current pseudo-version practice.Also restacked onto main (was 36 behind): reconciled the
go.mod/go.sum/provenance conflict to main's newer pin, dropped stale go.sum entries the auto-merge left, and absorbed SDK API drift since the branch's old7207f742snapshot (VerificationURIComplete/RegistrationEndpoint→*string).Summary by cubic
Adopts the BC5 OAuth stack end to end: resource‑first discovery, automatic RFC 8628 device‑flow login as public
basecamp-cli, and RFC 8707 resource echo, with stricter endpoint validation and provider‑neutral flags. Updates the SDK togithub.com/basecamp/basecamp-sdk/go@v0.10.1-0.20260729233651-9480aa4c84b8(merged stack: device flow, resource indicators, hardened transport/parse/refresh).New Features
basecamp-cliwith validated verification URLs (HTTPS, localhost case‑insensitive, ports 1–65535), sanitized display, and early abort on bad data;--device-codeforces headless;--scopehelp notes Launchpad ignores it.resourceacross refreshes for multi‑account tokens; mirrored in the SDK store; legacy “bc3” refreshes surface a re‑authenticate error.Migration
client.jsonis obsolete and safe to delete.Written for commit 2252fc1. Summary will update on new commits.