[COR-174] auth: shareable auth library + split-host config + RFC 8693 token exchange#1153
Open
[COR-174] auth: shareable auth library + split-host config + RFC 8693 token exchange#1153
Conversation
Introduces github.com/entireio/cli/auth as a shared OAuth client library for the Entire CLI. Three subpackages ship in this commit: * auth/tokens — TokenSet bundle plus unverified JWT claim parsing * auth/tokenstore — Store interface plus an OS-keyring reference impl * auth/deviceflow — RFC 8628 OAuth Device Authorization Grant client The packages are deliberately provider-agnostic: every server-specific value (endpoint paths, client_id, scope) is supplied at construction. The library has no global state, no implicit URLs, and no provider detection. It is intended to be importable by any RFC 8628 / RFC 8693 caller. No existing callers are wired up in this commit; the cmd/entire/cli shim swap follows separately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the bespoke device-flow client and keyring store in cmd/entire/cli/auth with thin wrappers over auth/deviceflow and auth/tokenstore. The package's exported API (NewClient, NewStore, DeviceAuthStart, DeviceAuthPoll, LookupCurrentToken, etc.) is preserved field-for-field so login.go / logout.go / auth.go don't need to change. Two wrapper concerns worth noting: 1. PollDeviceAuth maps the shared library's RFC 8628 §3.5 sentinel errors back to the wire-side error code in DeviceAuthPoll.Error. This keeps the existing polling loop in login.go (which switches on result.Error) working unchanged. 2. Store.GetToken keeps a backward-compatibility fallback for keyring entries written before this commit, which stored bare access-token strings rather than JSON-encoded TokenSets. SaveToken always writes the new shape; GetToken transparently handles both. The legacy decodeJSON / decodeJSONStrict tests are removed; equivalent coverage now lives in auth/deviceflow tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a transition-period env-var switch that picks between two device-flow configurations: v1 (default): /oauth/device/code + /oauth/token, client_id="entire-cli" v2 : /api/auth/oauth/device/code + /api/auth/token, client_id="cli" Both surfaces speak the same RFC 8628 protocol; only the paths and client_id differ. Default behaviour is unchanged. Setting ENTIRE_AUTH_PROVIDER_VERSION=v2 (alongside an appropriate ENTIRE_API_BASE_URL) opts a user into the next-generation surface early. Unrecognised values fall back to v1 so old binaries stay safe if a future v3 ever ships. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the fourth subpackage of the auth/ library: a small, provider- agnostic client for RFC 8693 token exchange. Caller supplies BaseURL, Path, and per-call ExchangeRequest fields (SubjectToken, SubjectTokenType, RequestedTokenType, plus optional Audience/Resource/Scope and an Extra url.Values for any non-standard form fields the server expects). The package defines constants only for RFC 8693's standard token-type URIs and the token-exchange grant_type — the requested-token-type URI is always caller-supplied. Returns *tokens.TokenSet on success with absolute ExpiresAt; wraps RFC 6749 / 8693 error responses with both code and description. Tests cover happy path, optional-field omission, Extra forwarding, standard-fields-override-Extra precedence, missing required fields, JSON and non-JSON server errors, missing access_token, and the no- expiry case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captive portals, corporate proxies, and VPN firewalls (Cloudflare WARP,
etc.) commonly intercept the OAuth endpoint and return a 200 OK with an
HTML error page. Today the JSON decoder produces an opaque error like:
start login: decode device auth start response: decode JSON response:
invalid character '<' looking for beginning of value
That tells the user nothing actionable. Now both auth/deviceflow and
auth/sts surface:
could not reach authentication server: server returned non-JSON
response (check VPN, proxy, or firewall — e.g. Cloudflare WARP)
Implementation lives in a new internal package auth/internal/oauthhttp.
Both deviceflow and sts now run their successful-response bodies
through oauthhttp.ReadAndDecodeJSON, which sniffs for a leading '<'
(after trimming whitespace) and returns a typed ErrNonJSONResponse
sentinel — callers can errors.Is when they want to branch, or just let
the message bubble up.
Tests cover the helper in isolation plus end-to-end paths through both
StartDeviceAuth, PollDeviceAuth, and Exchange.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The token endpoint's error response carries an optional human-readable
error_description alongside the standard error code (RFC 6749 §5.2,
inherited by RFC 8628 §3.5). The lib was decoding only the code, which
collapsed several distinct invalid_grant flavours — "device_code unknown"
vs "client_id does not match grant" vs already-consumed replay — into a
single opaque "device authorization failed: invalid_grant" at the CLI.
Pull through:
* auth/deviceflow now decodes both fields on a non-2xx response and
wraps the sentinel error as fmt.Errorf("%w: %s", sentinel, desc) so
errors.Is(err, ErrInvalidGrant) keeps matching while the message
retains the description.
* cmd/entire/cli/auth.DeviceAuthPoll grows an ErrorDescription field;
the shim extracts it from the wrapped sentinel.
* cmd/entire/cli/login.go appends ": <description>" to the user-facing
failure message when the server provided one.
Two new deviceflow tests cover the description-present and
description-absent paths (no trailing colon-space when absent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…version The auth-tokens endpoint family lives at different paths on the two backends — historical /api/v1/auth/tokens vs the consolidated /api/auth/tokens. ENTIRE_AUTH_PROVIDER_VERSION already gates the device-flow path split; auth_tokens.go now reads the same env var to pick its base path. ListTokens, RevokeToken, and RevokeCurrentToken all flow through one authTokensBasePath() helper so future paths land in one place. The env-var name is duplicated as a constant rather than imported from cmd/entire/cli/auth: api/ is a leaf package and shouldn't take a dependency on auth/ for routing. Both reads must stay in sync; flagged in a comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Across the auth/ library and customer-CLI shim, golangci-lint flagged a
fistful of routine findings that the existing files inherited or that
my recent commits introduced. None are correctness bugs; just noise
that the repo's strict configuration wants explicit suppression for.
* auth/deviceflow/deviceflow_test.go and auth/sts/sts_test.go grow a
shared writeBody(t, w, body) helper, replacing every `_, _ = io.WriteString`
in test fixtures. errcheck-clean without per-callsite nolints.
newTestClient drops its unused *httptest.Server return (unparam).
* auth/sts/sts.go suppresses gosec G101 on the three RFC 8693 standard
URI constants (GrantTypeTokenExchange, SubjectTokenType*) and
errcheck on the best-effort body read in readAPIError.
* auth/tokenstore/keyring.go suppresses gosec G117 on the json.Marshal
call that intentionally serialises the access token into the
OS-keyring entry (encrypted at rest by the OS).
* cmd/entire/cli/api/auth_tokens.go suppresses G101 on
authTokensProviderVersionEnvVar — env-var name, not a credential.
* cmd/entire/cli/auth/provider.go suppresses G101 on the v1/v2 entries
in the providers map (OAuth client_id and endpoint paths, not
credentials).
* cmd/entire/cli/auth/provider_test.go extracts wantClientIDV1 /
wantClientIDV2 test-local constants to satisfy goconst, then uses
them in every comparison.
cmd/entire/cli/auth/{client,store}.go also need nolint:wrapcheck
comments on the four shim returns — those changes sit in the working
tree alongside the in-progress AuthBaseURL refactor and will go in
together with that commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the CLI talk to deployments where the auth issuer and data API
live on different origins (e.g. us.console.partial.to mints tokens that
are then exchanged for partial.to-scoped tokens before each data-API
call).
Split-host plumbing:
- New ENTIRE_AUTH_BASE_URL env var; AuthBaseURL() falls back to
BaseURL() so single-host deployments are unchanged.
- Tokens are keyed in the keyring by the auth issuer (the host that
minted them), not by the data API URL.
- Auth-management commands (auth list/revoke/status/logout) hit the
auth host via NewClientWithBaseURL since their endpoints live there.
- Align v2 client_id to "entire-cli" to match v1.
New shareable library auth/tokenmanager:
- Provider-agnostic orchestration over auth/sts: cache, JWT-aud
shortcut, exchange dispatch.
- Config struct takes Issuer, ClientID, STSPath, Store, plus defaults
and test hooks. No globals, no env-var reads, no implicit URLs —
ready to share with other internal CLIs.
- TokenForResource/Token resolve to:
1) ErrNotLoggedIn when the store is empty,
2) core token verbatim when issuer == resource,
3) core token verbatim when its aud claim already includes the
resource (multi-audience tokens skip exchange),
4) RFC 8693 exchange otherwise, cached per (core, resource,
audience, requested-token-type, scope) until expiry.
CLI wiring:
- NewAuthenticatedAPIClient now takes ctx and routes through
tokenmanager so data-API calls carry the right-audience bearer.
All 7 callers updated to pass ctx.
- cmd/entire/cli/auth/exchange.go is a thin shim that builds a
package-level Manager from the active provider + NewStore() and
exposes TokenForResource / Token / ErrNotLoggedIn.
- *Store now implements tokenstore.Store so it can be passed to the
Manager, preserving the legacy bare-string keyring fallback.
Fix discovered along the way:
- search defaulted to a hardcoded entire.io serviceURL; now defaults
to api.BaseURL() when ENTIRE_SEARCH_URL is unset.
Misc gofmt/lint autofixes in auth/deviceflow, auth/sts, auth/tokens
that the linter applied while iterating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nager Two fixes that came out of getting `entire trail list` working against partial.to's split-host deployment: - Provider config now carries an stsPath alongside the OAuth token endpoint. v2's STS lives at /api/authz/sts/token, distinct from the /api/auth/token OAuth endpoint that rejects token-exchange grants with unsupported_grant_type. cmd/entire/cli/auth/exchange.go now passes provider.stsPath (rather than provider.tokenPath) into the tokenmanager. - v1 is the legacy single-host surface (entire.io for both auth and data API), so the same-host shortcut in tokenmanager.Token always wins and STS is never invoked. v1.stsPath is left empty. - tokenmanager.Config.STSPath is now optional. New() no longer rejects empty STSPath; runExchange() returns the new ErrNoSTSPath sentinel if an exchange is actually attempted with no path configured. Single-host setups (incl. v1) need no STS endpoint; split-host misconfigurations fail loudly at the right layer. Tests updated to cover empty-STSPath construction, the ErrNoSTSPath path, and v1's empty stsPath contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`entire search` was sending the raw core token to the search service, which on split-host deployments has the wrong audience (auth host issuer, not the data API). Switch to auth.TokenForResource(ctx, serviceURL) so the bearer is exchange-resolved against the search service URL: same-host shortcut keeps single-host setups unchanged, split-host setups now get an exchanged token with aud=entire-api. Also moves the auth lookup after the git/repo plumbing so the resource URL (which can come from ENTIRE_SEARCH_URL or api.BaseURL()) is known at the time we resolve the bearer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code review surfaced a real correctness bug plus a handful of clarity/coverage gaps. This commit fixes them in one pass. Critical fix — Store.LoadTokens legacy bare-string fallback was dead code: - tokenstore.Keyring.LoadTokens returned "unmarshal TokenSet: ..." for pre-shim bare-string entries, not ErrNotFound. The cmd-side shim's fallback only fired on ErrNotFound, so users with pre-shim keyring entries appeared logged out after upgrading to the manager-backed code path (entire trail/search/etc.). The legacy GetToken path was separately over-permissive: it fell back on any error, masking real keyring errors. - Add tokenstore.ErrMalformed sentinel returned (wrapped) by decodeTokenSet on JSON unmarshal or expires_at parse failures. - Update Store.LoadTokens / Store.GetToken to fall back precisely on ErrMalformed (legacy path) and surface ErrNotFound + real keyring errors verbatim. Regression tests pre-seed bare-string keyring entries and assert the round-trip. api.bearerTransport: reject empty bearer at first request rather than sending Authorization: Bearer<space> on the wire (which produces a confusing 401). New errEmptyBearerToken sentinel. api/auth_tokens: add table-driven test that pins the ENTIRE_AUTH_PROVIDER_VERSION → path mapping (v1/v2/unrecognised/ whitespace) plus an end-to-end ListTokens routing check. The path switch is the whole point of the version env var; it had no test. Doc fixes (review found these stale or misleading): - auth/doc.go: list tokenmanager subpackage (was missing). - auth/tokenstore/tokenstore.go: drop the "File impl" claim — only Keyring ships today. - auth/tokenstore/keyring.go: collapse the duplicated keyringTokenSet comment paragraph, drop the dangling G117 reference. - cmd/entire/cli/auth/exchange.go: defaultManager rationale corrected (sync.Once means later env-var changes are ignored, not honoured); TokenForResource doc points at Manager.Token (the rules live there, not on TokenForResource). - cmd/entire/cli/api_client.go: cache-key undercount — list all wire-affecting fields rather than just (core-token, resource). - cmd/entire/cli/search_cmd.go: rewrite the misleading "fall back to search.DefaultServiceURL" comment (the fallback is api.BaseURL()). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ions) Behaviour: - tokenmanager.DeleteCoreToken now deletes the keyring entry first and only clears the in-memory exchange cache on success. Pre-emptively clearing would leave a window where the CLI thinks it's logged out but the keyring still hands out the core token to the next process. Surfaces the store error wrapped as "delete core token: ...". Coverage: - tokenmanager: regression tests for the cache-clear (and its inverse — cache survives a failed delete), cache-key independence for RequestedTokenType and Scope (matching the existing Audience test), malformed-JWT fallthrough on the audience shortcut (security contract — corrupt cores must not be returned verbatim), and surface-don't-collapse for non-ErrNotFound store errors. Adds an erroringStore test helper for failure-path tests. - tokenstore.Keyring: pin the ErrMalformed contract — malformed JSON, legacy bare-string entries, and bad expires_at all surface as ErrMalformed (wrapped), not ErrNotFound. cmd-side legacy fallback depends on this distinction. Deprecations / docs: - Mark cmd/entire/cli/auth.Store.SaveToken/GetToken/DeleteToken as // Deprecated so godoc and IDE hover steer new callers to the tokenstore.Store interface methods. Legacy direct-bearer call sites (login, logout, auth status/list/revoke) keep using them; login.go carries a //nolint:staticcheck with a pointer to the doc. - Document keyringService = "entire-cli" as immutable — renaming would orphan every existing user's stored credentials. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a provider-agnostic, shareable auth library (auth/…) and migrates the CLI to use it, enabling split-host deployments (separate auth issuer and data API origins) via RFC 8693 token exchange.
Changes:
- Added a new root-level auth library implementing RFC 8628 device flow, RFC 8693 token exchange, token persistence, JWT claim parsing, and a token manager with caching/exchange orchestration.
- Added split-host configuration (
ENTIRE_AUTH_BASE_URL) and updated CLI auth/token storage and auth-management endpoints to route to the auth origin. - Updated CLI API client creation and
entire searchto resolve resource-scoped tokens (including URL-origin normalization) and threadcontext.Contextthrough authenticated client creation.
Reviewed changes
Copilot reviewed 40 out of 40 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/trail_cmd.go | Passes ctx into authenticated API client creation. |
| cmd/entire/cli/search/search.go | Clarifies GitHubToken field meaning for backwards compatibility. |
| cmd/entire/cli/search_cmd.go | Routes search via token manager; defaults service URL to api.BaseURL(); normalizes resource origin. |
| cmd/entire/cli/search_cmd_test.go | Adds coverage for search service URL → origin normalization. |
| cmd/entire/cli/recap.go | Passes ctx into authenticated API client creation. |
| cmd/entire/cli/logout.go | Routes logout revocation calls to auth base URL. |
| cmd/entire/cli/login.go | Uses legacy token save shim; surfaces RFC 8628 error_description on failures. |
| cmd/entire/cli/integration_test/login_test.go | Sets auth base URL + provider version for login integration tests. |
| cmd/entire/cli/dispatch_wizard.go | Passes ctx into authenticated API client creation. |
| cmd/entire/cli/auth/store.go | Wraps shared tokenstore.Keyring while preserving legacy bare-token fallback reads; keys tokens by auth issuer. |
| cmd/entire/cli/auth/store_test.go | Adds tests for legacy bare-string fallback behavior. |
| cmd/entire/cli/auth/provider.go | Introduces provider version switch (v1/v2) including per-provider STS path. |
| cmd/entire/cli/auth/provider_test.go | Tests provider selection and client wiring. |
| cmd/entire/cli/auth/exchange.go | Adds CLI shim over tokenmanager.Manager (singleton + test injection). |
| cmd/entire/cli/auth/exchange_test.go | Tests shim delegation and ErrNotLoggedIn aliasing. |
| cmd/entire/cli/auth/client.go | Replaces bespoke device-flow client logic with shared auth/deviceflow client + compatibility shim types. |
| cmd/entire/cli/auth/client_test.go | Removes tests for old JSON decoding helpers (now in shared oauthhttp helpers). |
| cmd/entire/cli/auth.go | Validates both API and auth origins for HTTPS; routes auth-management commands to auth base URL. |
| cmd/entire/cli/api/client.go | Adds NewClientWithBaseURL; fails early on empty bearer tokens at first request. |
| cmd/entire/cli/api/client_test.go | Adds test ensuring empty bearer tokens are rejected. |
| cmd/entire/cli/api/base_url.go | Adds ENTIRE_AUTH_BASE_URL and AuthBaseURL() fallback to BaseURL(). |
| cmd/entire/cli/api/base_url_test.go | Adds tests for AuthBaseURL() fallback/override behavior. |
| cmd/entire/cli/api/auth_tokens.go | Routes auth-token endpoints by provider version; documents v1/v2 differences. |
| cmd/entire/cli/api/auth_tokens_test.go | Adds tests for provider-version routing and v2 path selection. |
| cmd/entire/cli/api_client.go | Reworks authenticated API client creation to use token manager + RFC 8693 exchange when needed. |
| cmd/entire/cli/activity_cmd.go | Passes ctx into authenticated API client creation. |
| auth/tokenstore/tokenstore.go | Defines persistence interface and sentinel errors (ErrNotFound, ErrMalformed). |
| auth/tokenstore/keyring.go | Implements keyring-backed tokenstore.Store with JSON TokenSet encoding and malformed-entry signaling. |
| auth/tokenstore/keyring_test.go | Adds tests for keyring round-trips and malformed-entry behavior. |
| auth/tokens/tokens.go | Introduces TokenSet + unverified JWT claims parsing helpers. |
| auth/tokens/tokens_test.go | Adds tests for expiry helpers and JWT claim parsing. |
| auth/tokenmanager/tokenmanager.go | Adds manager orchestration: core token lookup, same-host / aud shortcuts, RFC 8693 exchange, caching. |
| auth/tokenmanager/tokenmanager_test.go | Adds comprehensive tests for exchange dispatch, caching, shortcuts, and failure modes. |
| auth/sts/sts.go | Adds RFC 8693 token-exchange client. |
| auth/sts/sts_test.go | Adds tests for form construction, error surfacing, and expiry handling. |
| auth/internal/oauthhttp/jsonresp.go | Adds shared JSON response decoding with HTML/non-JSON detection. |
| auth/internal/oauthhttp/jsonresp_test.go | Tests strict/tolerant decoding and HTML detection behavior. |
| auth/doc.go | Documents the new auth library package structure and goals. |
| auth/deviceflow/deviceflow.go | Adds RFC 8628 device-flow client with sentinel error mapping and strict/tolerant decoding rules. |
| auth/deviceflow/deviceflow_test.go | Adds tests for device-flow success/error behaviors, including error_description handling. |
`entire dispatch` was sending the raw core token (audience = auth host) to the data API and getting back a 401 that cloud.go mapped to "dispatch requires login — run \`entire login\`" — misleading on split-host deployments where the user IS logged in but with the wrong-audience bearer. Same trap search hit before; same fix. - mode_cloud.go now resolves the bearer via auth.TokenForResource so the tokenmanager's same-host shortcut / JWT-aud shortcut / RFC 8693 exchange all apply. ErrNotLoggedIn is mapped to the friendly "dispatch requires login" message; other errors surface verbatim. - mode_local.go grows a lookupResourceToken seam (defaulted to auth.TokenForResource) for test injection; the existing lookupCurrentToken seam is retained for back-compat with tests that haven't migrated. - Test stubs (stubCloudDispatchAuth + per-test cleanups) updated to swap both seams so the assertions still cover what they used to. Sweep confirmed no other data-API caller bypasses the manager: search, trail, recap, dispatch_wizard, and activity all flow through NewAuthenticatedAPIClient → tokenmanager. Auth-host commands (auth list/revoke/status, logout) correctly retain LookupCurrentToken since they need the auth-audience bearer. Docs: - New CLAUDE.md "Auth and token resolution" section flags the two blessed entry points (NewAuthenticatedAPIClient, TokenForResource), the resolution rules, and that LookupCurrentToken is for auth-host callers only. - New auth/README.md positions the library as shareable across internal CLIs: subpackage map, embedding checklist, design principles (no globals, no env-var reads, provider-agnostic), non-goals (OIDC discovery, server-side, code-flow PKCE), quick- start snippets for login / data-API call / logout. - search.Config.GitHubToken doc now points at TokenForResource (was LookupCurrentToken). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- PollDeviceAuth: unknown OAuth error codes (invalid_request, invalid_client, server_error, unsupported_grant_type, etc.) used to fall through to login.go's transient-retry path, burning ~25-150s on permanent server failures before producing a confusing "after N consecutive failures" message. Replace oauthErrorCode + descriptionFromSentinel with a single oauthErrorParts that also matches deviceflow's generic "oauth error: <code>" wrapper. Unknown codes now land in DeviceAuthPoll.Error so the polling loop's default switch arm fails fast with "device authorization failed: <code>". Tests cover known sentinels, sentinel-with-description, unknown- passthrough, unknown-with-description, and non-OAuth (transient) errors. - Store.GetToken: doc said "only ErrNotFound and ErrMalformed trigger the fallback" but ErrNotFound short-circuits to the empty-string return without a keyring read; only ErrMalformed actually triggers the bare-string fallback. Fixed the doc to match. - api.RevokeCurrentToken: dropped the stale comment about v2 not exposing /current — server-side fix is incoming. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
Two cursor bug-bot findings, both Low severity but worth closing. - auth/deviceflow + auth/sts: TestPollDeviceAuth_Success and TestExchange_Success call freezeClock, which mutates the package- level nowFunc. Both tests were marked t.Parallel(), creating a latent race against any future parallel test that reads nowFunc through a real Exchange/PollDeviceAuth call. Drop the t.Parallel() on those two tests with a comment explaining why; the rest of the package keeps parallelism. -race confirms no race remains. - auth/tokenmanager: cacheKey was a delimiter-joined string, structurally vulnerable to collisions if any field embedded the "|" separator (none do today, but no guarantee for future callers). Replace with a struct map key — Go's map can use comparable structs directly, so there's no string encoding to misbehave. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9c2b070. Configure here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
https://entire.io/gh/entireio/cli/trails/327
Summary
Linear: COR-174
Note
STS endpoint will be moving soon. v2 currently exposes the RFC 8693 token-exchange endpoint at
/api/authz/sts/token(encoded inprovider.go's v2 surface). Server-side work is in flight to relocate it; once that lands,provider.stsPathflips to the new path and the CLI side is a one-line change. No CLI release is needed in the interim — v1 is single-host and unaffected, and v2 staging deployments can stay pinned until the move completes.Stands up a shareable auth library at the repo root (
auth/...) and migrates the CLI's auth wiring onto it. Also enables the partial.to staging deployment where the auth issuer (us.auth.partial.to) and the data API (partial.to) live on different origins by introducing per-resource RFC 8693 token exchange.auth/library (new + reorganised):deviceflow(RFC 8628),sts(RFC 8693),tokens(TokenSet + JWT claim parsing),tokenstore(pluggable persistence +Keyringimpl), and the newtokenmanager(orchestration: cache, JWT-aud shortcut, exchange dispatch). The library is provider-agnostic — every endpoint, identifier, and default value comes fromConfig. No env-var reads, no globals, no implicit URLs. Ready to share with other internal CLIs.ENTIRE_AUTH_BASE_URLenv var;AuthBaseURL()falls back toBaseURL()so single-host deployments are unchanged. Tokens are now keyed in the keyring by the auth issuer (the host that minted them). Auth-management commands (auth list/revoke/status/logout) routed to the auth host since their endpoints live there.NewAuthenticatedAPIClientandentire searchgo through the manager. Data-API calls obtain a resource-audience bearer via RFC 8693 exchange when the core token's audience doesn't already match. All 7 callers threadctx. Search now defaultsserviceURLtoapi.BaseURL()(was hardcodedentire.io) and normalizes path-bearing URLs to scheme+host before token resolution.entire-cli(matched v1).stsPathper surface; v2's STS lives at/api/authz/sts/token. v1 is single-host, so itsstsPathis empty andtokenmanager.Config.STSPathis optional — the same-host shortcut wins. Misconfigured split-host setups fail loudly viaErrNoSTSPath.Behaviour fixes surfaced during review
Store.LoadTokenslegacy bare-string fallback was unreachable — it only fired onErrNotFound, but a pre-shim raw-token entry produced an unmarshal error. Addedtokenstore.ErrMalformedso callers can distinguish "no entry" from "entry exists but malformed" and route the legacy path correctly.Store.GetTokenwas over-permissive in the opposite direction (any error → fallback, masking real keyring failures); now also gated onErrMalformed.tokenmanager.runExchangewas silently droppingreq.Resource—sts.ExchangeRequest.Resource(RFC 8693 §2.1) was never sent to the AS. Fixed and tested.tokenmanager.Tokennow gated on emptyAudienceso an explicit per-callAudiencealways forces an exchange (was silently downgraded if the core token'saudhappened to include the resource).tokenmanager.DeleteCoreTokenorder swapped: keyring delete first, in-memory cache clear only on success — pre-emptive clear created a window where the CLI thought it was logged out but the keyring still held the token.api.bearerTransportrejects empty bearer at first request rather than puttingAuthorization: Bearer<space>on the wire (which produced confusing 401s).Test plan
mise run fmt && mise run lintcleango test ./auth/...— all passgo test ./cmd/entire/cli/auth/... ./cmd/entire/cli/api/...— all passentire login,entire trail list,entire searchall working through the device flow → STS exchange → resource-scoped bearer chaingo test ./cmd/entire/cli/...— only the two pre-existing failures (TestGroupCommitsByDay_SortsNewestFirst,TestExplainCmd_PositionalArgConflictsWithFlags) remain, unchanged frommainNotes for reviewers
auth/stspackage, RFC 8628 §3.5 error_description support, the v1/v2 provider switch, etc.). Easiest review path is by commit — the meaningful new work starts atc492a54b1:c492a54b1— split-host + tokenmanager (largest commit)d9322bc2a— STS path on provider,STSPathoptionalead027cf9— search routing16746fd66— round-1 review fixes (legacy fallback bug, doc rot, v2 path coverage, empty-bearer guard)5173d30fc— round-2 review fixes (DeleteCoreToken ordering, additional coverage, deprecation tags)f33b79dfc— codex review fixes (Audience-gates-aud-shortcut, Resource on STS, search URL normalization)d8ccd264a— test isolation against provider envauth/deviceflow.Client+auth/sts.Client, movingtokenmanager.Config.Now/Exchangeoff the public Config to test-only seams,TokenRequest.Resource→ResourceURLrename,tokens.ParseClaimsvalue-return, typed-error replacement ofdescriptionFromSentinel, exposetokenstore.Keyring.Service()as a getter.🤖 Generated with Claude Code
Note
High Risk
High risk because it rewires core authentication/token storage and introduces RFC 8693 token exchange plus new base-URL routing, which can cause login failures or 401s across CLI commands if misconfigured.
Overview
Adds a new shareable
auth/Go library implementing OAuth device flow (RFC 8628), token exchange (RFC 8693), token persistence, and atokenmanagerthat caches resource-scoped bearers and short-circuits when exchange isn’t needed.Updates the CLI to support split auth/data origins via
ENTIRE_AUTH_BASE_URLand provider-version routing viaENTIRE_AUTH_PROVIDER_VERSION, moving authenticated calls (e.g.NewAuthenticatedAPIClient,search,dispatch) to resolve per-resource tokens through the manager rather than reading the keyring directly.Refactors keyring storage to JSON-encoded token bundles with explicit
ErrMalformedhandling and legacy bare-string fallback, routes auth-token management endpoints to the auth host, and tightens request safety (reject empty bearer, enforce secure URL checks for both auth and data hosts).Reviewed by Cursor Bugbot for commit 9c2b070. Configure here.