Skip to content

Integrate auth-go: split-host config + RFC 8693 token exchange#1239

Open
Soph wants to merge 18 commits into
mainfrom
soph/auth-go
Open

Integrate auth-go: split-host config + RFC 8693 token exchange#1239
Soph wants to merge 18 commits into
mainfrom
soph/auth-go

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented May 20, 2026

https://entire.io/gh/entireio/cli/trails/403

Summary

  • Migrates the CLI's inline OAuth implementation to the shared github.com/entireio/auth-go library, picking up its hardened OAuth client (origin-URL validation, alg:none JWT rejection, HTTPS-only with loopback exception, verification_uri validation, centralised error parsing).
  • Adds split-host config: ENTIRE_AUTH_BASE_URL for the auth issuer, falling back to ENTIRE_API_BASE_URL so single-host deployments are unchanged. Split-host setups (e.g. us.auth.partial.to mints tokens, partial.to consumes them) get RFC 8693 token exchange transparently — the data-API bearer is minted on demand from the core token via the auth host's STS endpoint.
  • Introduces a provider routing table (v1/v2 via ENTIRE_AUTH_PROVIDER_VERSION) so endpoint paths and the auth-tokens management URL are configured in one place instead of duplicated between auth/ and api/.

What changed under the hood

Library integration

  • Pinned github.com/entireio/auth-go v0.1.1-0.20260520123811-da94c52ef430 (PR use strings.Contains #5 head of that repo).
  • auth/client.go is now a thin shim over deviceflow.Client. auth/store.go implements tokenstore.Store. auth/exchange.go wires a package-level tokenmanager.Manager and exposes TokenForResource(ctx, url) / Token(ctx, req).
  • All eight data-API callers (activity, dispatch_wizard, recap, search completion, trail ×4, watch) now thread cmd.Context() and obtain bearers via auth.TokenForResource. Dispatch's cloud mode and recap join the same path.

Security hardening

  • Login validates the received token before persisting: rejects alg:none JWTs (tokens.ErrUnsignedJWT), enforces iss matches the configured issuer, refuses pre-expired tokens. Opaque (non-JWT) tokens are accepted unconditionally — every parse failure other than ErrUnsignedJWT is treated as opaque per the doc-comment promise.
  • api.AuthBaseURL() canonicalises to match tokenmanager's internal normalisation (lowercase scheme/host, strip default port, drop path/query/fragment), so the keyring key login wrote matches the one the manager later reads byte-for-byte.
  • bearerTransport omits the Authorization header when the bearer is empty rather than sending a malformed "Authorization: Bearer ". This preserves recap's "let the server return a typed 401" UX without exposing a malformed header.
  • --insecure-http-auth is threaded through both the deviceflow client (auth.NewClient(httpClient, allowInsecureHTTP)) and the tokenmanager (auth.EnableInsecureHTTP() setter), so split-host local-dev on plain HTTP works end-to-end. Loopback hosts remain permitted unconditionally either way.

Code quality

  • New api.OriginOnly / api.NormalizeOriginURL helpers replace what would have been three copy-paste sites for stripping URLs to their origin (in search_cmd.go's search-host resolution, login.go's iss comparison, and api.AuthBaseURL).
  • newAuthHostAPIClient(token) deduplicates the auth-token management endpoint construction across status/list/revoke/logout.

Test plan

  • mise run fmt && mise run lint → 0 issues.
  • mise run test — 5294 unit tests pass, 3 skipped.
  • mise run test:integration — 348 integration tests pass.
  • mise run test:ci — full CI (unit + integration + Vogon canary) exit 0.
  • Manual: verify split-host deployment against the partial.to staging environment (ENTIRE_AUTH_BASE_URL=https://us.auth.partial.to, ENTIRE_API_BASE_URL=https://partial.to) — entire login, entire trail list, entire dispatch should all exchange through the STS endpoint.
  • Manual: verify entire login --insecure-http-auth against a non-loopback http auth host (e.g. ENTIRE_AUTH_BASE_URL=http://devbox.internal:8787) — previously failed with ErrInsecureBaseURL; should now complete.
  • Manual: verify entire recap when logged out — the request should reach the server (no Authorization header) and recap should render its "sign in" message rather than a transport error.
  • Manual: verify entire login with a non-canonical issuer (ENTIRE_AUTH_BASE_URL=https://AUTH.example.com:443/) — the saved keyring entry should be readable by subsequent entire trail list / entire status etc. (no spurious "not logged in").

Related work

  • Reference: this is a fresh implementation of #1153's integration on top of auth-go PR #5's hardening. Once auth-go PR use strings.Contains #5 ships as v0.2.0, the go.mod pseudo-version should bump to the released tag.

🤖 Generated with Claude Code


Note

High Risk
Touches authentication, token storage, STS exchange, and every data-API command; misconfiguration or exchange bugs would cause widespread login/API failures.

Overview
Replaces the CLI’s inline OAuth stack with github.com/entireio/auth-go, adding split-host support via ENTIRE_AUTH_BASE_URL (auth issuer / keyring key) separate from ENTIRE_API_BASE_URL (data API). Data-plane commands now resolve bearers through auth.TokenForResource (RFC 8693 exchange when the stored token’s audience doesn’t match the resource); ENTIRE_AUTH_PROVIDER_VERSION (v1/v2) centralizes device-flow, STS, and auth-token paths.

Login & storage: device flow is a shim over deviceflow.Client; tokens are keyed by canonical api.AuthBaseURL(); login validates JWTs (reject alg:none, iss match, not pre-expired) and prefers verification_uri_complete for the browser URL.

API clients: NewAuthenticatedAPIClient takes context.Context and uses exchange; auth token list/revoke hit api.BaseURL() with WithAuthTokensPath from the provider table; empty bearer omits Authorization (recap logged-out path).

Commands: activity, search (+ --insecure-http-auth), dispatch cloud mode, recap, trail/watch, and dispatch wizard thread context and the new token path; recap no longer mislabels exchange/keyring failures as keyring-only errors.

Reviewed by Cursor Bugbot for commit d1f0497. Configure here.

Copilot AI review requested due to automatic review settings May 20, 2026 20:35
@Soph Soph requested a review from a team as a code owner May 20, 2026 20:35
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request migrates the CLI’s authentication flow to the shared github.com/entireio/auth-go library, adds split-host auth configuration (ENTIRE_AUTH_BASE_URL), and enables RFC 8693 token exchange so data-API calls can transparently obtain resource-scoped bearer tokens in split-host deployments.

Changes:

  • Integrate auth-go (device flow + token manager) and route data-API callers through auth.TokenForResource(ctx, resource).
  • Add split-host configuration via api.AuthBaseURL() (with canonicalization) and provider routing (ENTIRE_AUTH_PROVIDER_VERSION).
  • Harden request behavior and UX (omit empty Authorization header; validate received JWTs and reject alg:none; centralize auth-tokens endpoint routing).

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
go.mod Bumps Go version and adds github.com/entireio/auth-go dependency.
go.sum Adds checksums for auth-go.
cmd/entire/cli/api_client.go Updates authenticated client construction to use token exchange via TokenForResource.
cmd/entire/cli/api/base_url.go Adds ENTIRE_AUTH_BASE_URL + origin canonicalization helpers.
cmd/entire/cli/api/base_url_test.go Adds tests for auth base URL fallback and canonicalization.
cmd/entire/cli/api/client.go Adds auth-tokens base-path support; omits Authorization header when token is empty.
cmd/entire/cli/api/client_test.go Tests omission of Authorization header for empty bearer.
cmd/entire/cli/api/auth_tokens.go Routes auth-token management endpoints via configurable base path.
cmd/entire/cli/api/auth_tokens_test.go Updates tests to set auth-tokens base path.
cmd/entire/cli/auth.go Checks both data/auth origins for HTTPS; centralizes auth-host API client construction.
cmd/entire/cli/auth/client.go Replaces inline device-flow logic with auth-go/deviceflow shim and OAuth error parsing.
cmd/entire/cli/auth/client_test.go Adds tests for OAuth error parsing, expiry computation, and insecure HTTP allowances.
cmd/entire/cli/auth/exchange.go Adds tokenmanager singleton wiring + TokenForResource/Token shims and insecure HTTP override.
cmd/entire/cli/auth/exchange_test.go Tests insecure HTTP override flag behavior.
cmd/entire/cli/auth/provider.go Adds provider routing table and one-time provider selection.
cmd/entire/cli/auth/provider_test.go Tests provider resolution and test override hook.
cmd/entire/cli/auth/store.go Implements tokenstore.Store and keys tokens by api.AuthBaseURL().
cmd/entire/cli/login.go Threads insecure HTTP into auth client; validates received tokens; surfaces error_description.
cmd/entire/cli/login_validate_test.go Adds unit tests for received-token validation behavior.
cmd/entire/cli/logout.go Routes revocation/logout to auth host (AuthBaseURL) and provider-routed auth-tokens path.
cmd/entire/cli/recap.go Resolves resource-scoped bearer via token exchange; preserves logged-out “send request without auth header” path.
cmd/entire/cli/search_cmd.go Uses token exchange for search bearer; defaults service host to api.BaseURL().
cmd/entire/cli/activity_cmd.go Threads ctx into authenticated client creation.
cmd/entire/cli/dispatch_wizard.go Threads ctx into authenticated client creation.
cmd/entire/cli/dispatch/mode_local.go Switches dispatch auth wiring to resource-scoped token lookup.
cmd/entire/cli/dispatch/mode_cloud.go Uses token exchange for dispatch bearer; wires insecure HTTP override.
cmd/entire/cli/dispatch/mode_cloud_test.go Updates cloud mode tests to stub resource token lookup.
cmd/entire/cli/dispatch/dispatch_test.go Updates tests for new “not logged in” sentinel behavior.
cmd/entire/cli/trail_cmd.go Threads ctx into authenticated client creation.
cmd/entire/cli/trail_watch_cmd.go Threads ctx into authenticated client creation.
Comments suppressed due to low confidence (1)

cmd/entire/cli/recap.go:200

  • auth.TokenForResource is called with api.BaseURL(). If ENTIRE_API_BASE_URL is set to a URL with a path component, token resolution may fail because the token manager expects an origin-only resource URL. Consider passing api.OriginOnly(api.BaseURL()) here (and using the same origin-only value for the later RequireSecureURL check) to make recap robust to path-bearing base URLs.
func newRecapClient(ctx context.Context, insecureHTTP bool) (*api.Client, error) {
	if insecureHTTP {
		auth.EnableInsecureHTTP()
	}
	token, err := auth.TokenForResource(ctx, api.BaseURL())
	if errors.Is(err, auth.ErrNotLoggedIn) {
		token = ""
		err = nil
	}
	if err != nil {
		return nil, &keyringReadError{Cause: err}
	}
	if token != "" && !insecureHTTP {
		if err := api.RequireSecureURL(api.BaseURL()); err != nil {
			return nil, fmt.Errorf("base URL check: %w", err)

Comment thread cmd/entire/cli/auth/client_test.go
Comment thread cmd/entire/cli/auth/client.go Outdated
Comment thread cmd/entire/cli/api_client.go
Comment thread cmd/entire/cli/dispatch/mode_cloud.go Outdated
Soph and others added 12 commits May 21, 2026 15:30
Add ENTIRE_AUTH_BASE_URL env var and api.AuthBaseURL() helper, falling
back to api.BaseURL() so single-host deployments are unchanged. Split-
host setups (e.g. us.auth.partial.to mints tokens, partial.to consumes
them) set both.

Also adds api.OriginOnly() to strip path/query/fragment to scheme+host
— preparing the way for callers to feed origin-only URLs into
tokenmanager.TokenRequest.Resource and to compare JWT iss claims.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b3ae7de91302
Introduces auth.CurrentProvider() and a Provider struct holding the
per-surface endpoint paths and client_id. ENTIRE_AUTH_PROVIDER_VERSION
selects v1 (legacy /oauth/... single-host) or v2 (/api/auth/... with
RFC 8693 STS at /api/authz/sts/token).

Resolution is single source of truth for endpoint routing — the api/
package no longer reads ENTIRE_AUTH_PROVIDER_VERSION itself; auth-token
management endpoints receive their path via (*api.Client).WithAuthTokensPath
in later commits. SetProviderForTest() injects an override under a mutex
so parallel tests can run with different providers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 664f67c941ad
api: drop the hardcoded /api/v1/auth/tokens path. Add NewClientWithBaseURL
so callers can target the auth host (separate from the data API host)
and (*Client).WithAuthTokensPath so the routing comes from the provider
table rather than from a duplicated env var. bearerTransport refuses
empty bearers up-front — otherwise an unconstructed client would put
'Authorization: Bearer ' on the wire and produce a confusing 401.

cli: route auth status/list/revoke/logout to api.AuthBaseURL() (the
host that minted the token) and supply the path via the new
newAuthHostAPIClient() helper. requireSecureBaseURL now validates both
the data and auth origins, skipping the second parse when they're the
same string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 21fd1c9081fc
…693 exchange

Pins github.com/entireio/auth-go to its hardened OAuth client (PR #5
head), which validates Resource as an origin URL, refuses unsigned JWTs
(alg:none), enforces HTTPS unless the host is loopback, validates the
verification_uri returned by the AS, and centralises OAuth error
parsing with sanitised descriptions.

auth/client.go: now a thin shim over deviceflow.Client. Preserves the
historical DeviceAuthStart/DeviceAuthPoll types as aliases / equivalent
shapes so login.go's polling switch keeps working. Unknown OAuth error
codes are surfaced through DeviceAuthPoll.Error/ErrorDescription so
login.go can fail fast on terminal rejections instead of treating them
as transient.

auth/store.go: implements tokenstore.Store (SaveTokens/LoadTokens/
DeleteTokens) so it can be passed to tokenmanager.New. Adds a defensive
JSON-shape check on read — pre-shim entries are opaque token strings,
never JSON; a JSON blob in the keyring is corruption and must not be
put on the wire as a bearer.

auth/exchange.go: new package-level Manager wired from CurrentProvider,
AuthBaseURL, and NewStore. Exposes TokenForResource(ctx, url) and Token
(ctx, req); both go through the manager's same-host shortcut, JWT-aud
shortcut, and STS exchange dispatch as appropriate. SetManagerForTest()
takes a mutex so concurrent tests can swap without racing.

Single-host deployments hit the same-host shortcut and never call STS;
split-host deployments perform an RFC 8693 token exchange against the
auth issuer's STS endpoint to mint a resource-scoped bearer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 6d696fc55440
NewAuthenticatedAPIClient now takes a context and resolves the bearer
via auth.TokenForResource(ctx, api.BaseURL()). In single-host setups
this is a same-host shortcut returning the core token unchanged; in
split-host setups (ENTIRE_AUTH_BASE_URL ≠ ENTIRE_API_BASE_URL) it
performs an RFC 8693 exchange so the bearer carries the data-API
audience rather than the auth-host one.

Threads cmd.Context() through all eight call sites: activity,
dispatch_wizard, recap, search completion, trail (×4), trail watch.

recap and dispatch's cloud mode get the same treatment, with
auth.ErrNotLoggedIn collapsed back into the right user-visible
message at each call site:
  - recap turns it into an empty token (FetchMeRecap surfaces 401s
    via recapLoadErrorMessage so flag effects and the real auth
    error aren't collapsed into one "sign in" hint);
  - dispatch/cloud returns "dispatch requires login — run `entire
    login`" as before.

dispatch's lookupCurrentToken test seam is replaced by
lookupResourceToken; the four test sites and stubCloudDispatchAuth
helper are updated to inject via the new variable.

search additionally falls back to api.BaseURL() when ENTIRE_SEARCH_URL
is unset (instead of search.DefaultServiceURL) so a custom data host
applies to search too, and strips path/query via api.OriginOnly before
calling TokenForResource — the lib validates Resource as a strict
origin URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: a45484da26df
Defense in depth against a compromised or misconfigured authorization
server. The server is still the authority on signature/exp validation
on every request; these checks are minimum-trust shape checks that
refuse to write a clearly-bogus token to the keyring.

validateReceivedToken parses the token as a JWT (via tokens.ParseClaims
from auth-go, which refuses alg:none per RFC 7518 §3.6) and:

  - lets opaque (non-JWT) tokens through — the AS may not issue JWTs
    at all, and the server-side validation will catch issues there;
  - rejects unsigned (alg:none) JWTs unconditionally — the unsigned
    shape would let a hostile AS bypass shape checks;
  - rejects tokens whose `iss` claim doesn't match the issuer we sent
    the device-code request to, normalising both sides through
    api.OriginOnly so trailing-slash differences match;
  - rejects tokens whose `exp` claim is already in the past — a token
    that's pre-expired before we even store it is a smell.

Also threads ErrorDescription from the polling response through to the
final error message — previously unknown OAuth error codes lost the
server-supplied description.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 755abeeac77f
Before this fix, --insecure-http-auth disabled the CLI-side HTTPS guard
but the new deviceflow.Client still hard-coded AllowInsecureHTTP to
loopback-only — so entire login --insecure-http-auth against a non-
loopback http host (e.g. http://devbox.internal:8787) would pass the
CLI guard and then fail with ErrInsecureBaseURL deeper in the lib. The
hidden flag's whole purpose is local-dev auth hosts on private networks,
so this regressed an existing dev workflow.

auth.NewClient now takes an allowInsecureHTTP bool that's OR'd with
isLoopbackHTTP: loopback remains permitted unconditionally, and the
flag adds non-loopback http on top. The CLI plumbs insecureHTTPAuth
through from the login command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 751509e33813
tokenmanager.New normalises its Issuer config via
oauthhttp.NormalizeOriginURL — lowercase scheme/host, strip default
port, drop path/query/fragment, collapse trailing slash — before using
it as the tokenstore profile key. Without matching normalisation on the
CLI side, an env value like ENTIRE_AUTH_BASE_URL=https://AUTH.example.com:443/
made login save under one key (the raw form) and every subsequent
data-API command probe under another (the normalised form), surfacing
as a spurious "not logged in" loop that survives until the user logs in
under a canonical URL.

api.NormalizeOriginURL mirrors the lib's logic so the value the CLI
hands to tokenmanager as Issuer survives the manager's own normalisation
pass byte-for-byte. AuthBaseURL() applies it; OriginOnly is now an alias
so the search/login call sites that strip arbitrary URLs to origins get
the same canonical form for free.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 82c48025b2e0
The previous switch only treated tokens.ErrMalformedJWT (not-3-segments)
as opaque; anything that failed deeper in ParseClaims — 3-segment-but-
not-base64, payload-not-JSON, header-not-JSON, bad aud claim — fell
through to a hard "parse claims" error and rejected the login. The
doc-comment promised opaque tokens were allowed but the code rejected
plenty of them.

Restructured so only tokens.ErrUnsignedJWT is a hard refusal (alg:none
is unsafe regardless); every other parse failure is treated as opaque
and accepted, with the server-side validation as the authority. Two
regression tests cover the 3-segment-opaque and bad-base64-payload
paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 27078ed75606
The previous guard rejected any request constructed via NewClient("")
locally, breaking recap's intentional logged-out flow: recap deliberately
constructs an empty-token client so the request reaches the server,
which then returns a typed 401 that recap's UI handles specially with
a "sign in to use recap" message. With the guard in place, recap's fetch
failed locally with errEmptyBearerToken — never reaching the server —
and the user saw a confusing transport error instead.

The original concern (a malformed "Authorization: Bearer " on the wire
producing confusing server-side 401s) is still defended: when token is
empty, the Authorization header is simply omitted rather than sent as
"Bearer ". Callers that need a local fast-fail for missing auth should
check ErrNotLoggedIn at construction time, not rely on the transport.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b98bd20e1bf4
Data-API commands (trail, dispatch, recap, activity, search completion)
skip the top-level HTTPS guard when --insecure-http-auth is set, but
auth.TokenForResource was building its package-level Manager with
AllowInsecureHTTP enabled only for loopback issuers. auth-go's
ValidateOriginURL then rejected non-loopback http:// resources during
token resolution, so split-host local-dev setups on plain HTTP failed
before any request was sent — the flag was accepted but had no effect
on this code path.

auth.EnableInsecureHTTP() is a setter that flips a package-level atomic
read by defaultManager on first construction. Loopback hosts remain
permitted unconditionally; the flag adds non-loopback http on top so a
local-dev box reachable as http://devbox.internal works. The three call
sites that own the --insecure-http-auth flag (NewAuthenticatedAPIClient,
newRecapClient, dispatch's runServer) invoke the setter before any
TokenForResource call when the flag is set.

The setter is fire-and-forget by design: the manager's AllowInsecureHTTP
freezes on first construction, so callers must opt in before their first
token resolution. Tests bypass exchange.go entirely via the
managerForTest seam, so the override has no effect on test paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 1b6b7a3252f9
…lamp secondsUntil

Three call sites — NewAuthenticatedAPIClient, newRecapClient, and
dispatch's runServer — fed raw api.BaseURL() into auth.TokenForResource.
tokenmanager validates Resource as a strict origin URL, so a deployment
with ENTIRE_API_BASE_URL=https://api.example.com/v1 (path-bearing) would
fail token resolution before any request was sent. api.OriginOnly()
strips the path; search_cmd.go already did this, so the three other
sites now match.

secondsUntil could return a negative integer when ExpiresAt was already
in the past (clock skew, scheduling delay, tests overriding nowFunc to
a future time then querying an already-elapsed expiry). Historically
the deviceflow polling response carried a non-negative expires_in;
downstream loggers and any code displaying remaining lifetime didn't
expect a negative value. max(0, ...) clamps the result and a new
regression test covers the past-expiry path.

TestSecondsUntil_FutureExpiry mutates the package-level nowFunc under
t.Parallel(), which races any other parallel test (including the new
TestSecondsUntil_PastExpiryClampsToZero) that also reads it. Dropping
t.Parallel() on both tests is the minimum fix — a deeper refactor would
take a clock parameter, but the call-site is internal-only and the
mutation is bounded by t.Cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 65e8747d0752
Soph and others added 3 commits May 21, 2026 20:40
…to data host

Verified against us.auth.partial.to/.well-known/openid-configuration —
the v2 surface follows RFC 6749/8628/8693 standard endpoints, not the
"/api/auth/*" namespace originally guessed in PR #1153.

provider.go:
  - DeviceCodePath:   /api/auth/oauth/device/code → /device_authorization
  - TokenPath:        /api/auth/token             → /oauth/token
  - STSPath:          /api/authz/sts/token        → /oauth/token
    (RFC 8693 dispatches at the standard token endpoint via grant_type)
  - AuthTokensPath:   /api/auth/tokens            → /api/v1/auth/tokens
    (API tokens are an application resource, not an OIDC concept —
    they live on the data API in both v1 and v2 deployments.)

auth.go / logout.go: renamed newAuthHostAPIClient to newAPITokensClient
and pointed it at api.BaseURL() instead of api.AuthBaseURL(). The auth
host (entire-core in v2) is an OIDC provider — it mints OAuth tokens
but doesn't host application API token management endpoints. The
keyring key still uses AuthBaseURL (correct — it identifies the issuer
that minted the credential), but the list/revoke/current API calls
target the data API where the api_tokens table actually lives.

Single-host deployments (entire.io) are unaffected: AuthBaseURL falls
back to BaseURL so the URLs resolve identically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 0601e26bd7c5
RFC 8628 §3.3.1: the authorization server SHOULD return a
verification_uri_complete that embeds the user_code in the query so the
verification page can prefill the input. The CLI was opening the bare
verification_uri unconditionally, dropping the code on the floor — the
user always had to type it back in from the terminal even when the AS
had given us a click-through URL that would have prefilled it.

chooseApprovalURL prefers the complete form when set and falls back to
the bare URI otherwise. Both partial.to (single-host: emits ?code=) and
us.auth.partial.to (split-host: emits ?user_code= per RFC) supply a
complete form, so this prefills correctly on entire.io / partial.to
and shows the code in the address bar on entire-core (its SPA reads
?code= rather than the RFC-standard ?user_code= so prefill still
misses, but that's an entire-core SPA fix — code is in the URL either
way which is the RFC intent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: e7595451f64b
Resolves conflict in go.mod: keeps the entireio/auth-go dependency
added by 49dca72 (Integrate auth-go library) while taking the newer
go-git/go-git/v6 and go-git/go-billy/v6 versions from main.

While here, advance the auth-go pin from the pre-release commit
v0.1.1-0.20260520123811-da94c52ef430 to the v0.3.3 release tag. The
intermediate v0.3.x versions deliver three fixes the entire CLI's
split-host (us.auth.entire.io issuer, entire.io data API) deployment
depends on:

  v0.3.1 (auth-go #6): HTTP Basic Auth for the token-exchange grant —
    zitadel-oidc reads client credentials only from Authorization:
    Basic on this grant, not the form body.
  v0.3.2 (auth-go #7): default SubjectTokenType to :access_token —
    zitadel-oidc's STS validator handles :access_token / :refresh_token
    / :id_token but NOT :jwt, so the prior URI was silently rejected
    as "subject_token is invalid".
  v0.3.3 (auth-go #8): default Audience to Resource on the wire —
    zitadel-OIDC-backed servers reject token-exchange requests without
    an explicit `audience` form field with "invalid_target: audience
    is required".

End-to-end verified against us.auth.entire.io: entire login, search,
activity all succeed.
@khaong
Copy link
Copy Markdown
Contributor

khaong commented May 24, 2026

Pushed 61671af19: merged origin/main into the branch and bumped auth-go from the pre-release v0.1.1-0.20260520123811-da94c52ef430 to the v0.3.3 release tag.

The intermediate v0.3.x releases deliver three fixes the entire CLI's split-host (us.auth.entire.io issuer, entire.io data API) deployment depends on:

  • v0.3.1 (auth-go #6) — HTTP Basic Auth for the token-exchange grant. zitadel-oidc reads client credentials only from Authorization: Basic on this grant, not the form body, so the prior form-only request shape was rejected with invalid_client.
  • v0.3.2 (auth-go #7) — default SubjectTokenType to :access_token. zitadel-oidc's STS validator only handles :access_token / :refresh_token / :id_token; :jwt silently falls through and surfaces as subject_token is invalid even when the underlying token is a perfectly valid signed JWS access token.
  • v0.3.3 (auth-go #8) — default Audience to Resource when caller didn't set it. zitadel-OIDC-backed servers (entire-core) reject token-exchange requests without an explicit audience form field with invalid_target: audience is required.

End-to-end verified against us.auth.entire.io: entire login, entire search, entire activity all succeed.

Note on scope: entire auth status / auth list / auth revoke / logout (server-side revoke) on v2 split-host still need a separate audience-correctness fix — they currently send the raw keyring token (auth-host audience) to the data host, which 401s. That refactor is on a follow-up PR: soph/auth-status-followup (PR coming). v1 single-host is unaffected since there's no audience mismatch possible.

🤖 Generated with Claude Code

v0.3.4 brings the soph/more-improvements-fixes backlog into the
release stream and ships a v0.3.4 CHANGELOG. No new behavior changes
that affect the CLI's wire shape — the v0.3.x audience/Basic-Auth/
SubjectTokenType fixes that unblocked split-host already shipped in
v0.3.1 through v0.3.3.

Notable hardening from the backlog merge:
  - Centralised OAuth HTTP helpers (no caller-visible change)
  - Tightened resource path validation (reject multi-slash paths)
  - Config.Issuer held to the same origin-URL contract as Resource —
    the CLI already passes api.NormalizeOriginURL'd URLs, so safe.
  - Terminal-escape sanitisation on error codes and verification URIs
  - Extra["client_id"] hardening — we keep sending the matching value
    via tokenmanager.runExchange, so safe.

End-to-end re-verified against us.auth.entire.io: entire login,
entire search, entire activity still succeed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@khaong
Copy link
Copy Markdown
Contributor

khaong commented May 25, 2026

bugbot run

Comment thread cmd/entire/cli/recap.go
Comment thread cmd/entire/cli/search_cmd.go Outdated
khaong and others added 2 commits May 25, 2026 12:49
Pre-refactor, newRecapClient called auth.LookupCurrentToken — a pure
keyring read. Wrapping the failure as *keyringReadError and showing
the user "could not read your auth token from the system keyring"
was correctly targeted at that narrow case.

The auth-go refactor swapped LookupCurrentToken for TokenForResource,
which can fail for many reasons that have nothing to do with the
keyring: RFC 8693 token-exchange rejected by the AS, network error
reaching the auth host, audience misconfiguration on the AS, HTTPS
guard rejection. All of them were still being relabelled as keyring
read failures, sending users on wild goose chases checking their OS
keychain settings when the real problem was downstream.

Drop the keyringReadError type and its switch-case in runRecap; let
the underlying error surface verbatim through the default branch.
main.go's error printer is honest about what actually went wrong.
api.ErrInsecureHTTP still gets its targeted message (different code
path, fired by api.RequireSecureURL after token resolution).

Same wild-goose-chase shape we already fixed in activity_cmd.go;
recap.go was missed on the first pass.

Addresses Cursor Bugbot finding on PR #1239.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Other commands that go through auth.TokenForResource — login, logout,
activity (via NewAuthenticatedAPIClient), recap, dispatch — all wire
the existing --insecure-http-auth opt-in so split-host local-dev
setups with a non-loopback http:// auth host (e.g.
ENTIRE_AUTH_BASE_URL=http://devbox.internal:8787) can opt into the
tokenmanager's relaxed HTTPS guard. Search was the one outlier;
without the flag it would silently fail at the tokenmanager layer
even after the user passed --insecure-http-auth to other commands in
the same session.

Wire the flag the same way the rest of the CLI does:
addInsecureHTTPAuthFlag registers the (hidden) flag, the closure
calls auth.EnableInsecureHTTP() before TokenForResource if set. Token
resolution extracted into a resolveSearchToken helper to keep
newSearchCmd's complexity below the maintidx threshold; mechanical
extraction, no behavior change.

Addresses Cursor Bugbot finding on PR #1239.

Note: the broader question of whether auth.EnableInsecureHTTP's
process-wide setter pattern is the right shape — vs. an env var or
a context-scoped value — is a wider dev-environment discussion
flagged separately; this commit closes the symmetry gap Bugbot
caught without taking a position on that.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@khaong
Copy link
Copy Markdown
Contributor

khaong commented May 25, 2026

bugbot run

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d1f0497. Configure here.

Comment thread cmd/entire/cli/activity_cmd.go
@khaong khaong enabled auto-merge May 25, 2026 05:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants