Skip to content

auth: review follow-ups (provider routing, URL normalization, expiry preflight, HTTP timeouts)#1156

Merged
khaong merged 1 commit into
alex/cli-auth-consolidationfrom
alex/cli-auth-followup-fixes
May 14, 2026
Merged

auth: review follow-ups (provider routing, URL normalization, expiry preflight, HTTP timeouts)#1156
khaong merged 1 commit into
alex/cli-auth-consolidationfrom
alex/cli-auth-followup-fixes

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented May 8, 2026

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

Summary

Six fixes called out across the security / architecture / correctness reviews of #1153. Each lands with focused tests; no behaviour change outside the auth path. Targets alex/cli-auth-consolidation so it can ride along with that PR (rebase before merging the parent, or merge this first into the parent branch).

Fixes

  1. Eliminate duplicate ENTIRE_AUTH_PROVIDER_VERSION read in cmd/entire/cli/api/auth_tokens.go. Provider table now owns AuthTokensPath; api.Client takes it via WithAuthTokensPath. The api package no longer reads the env var.
  2. Read provider version once at startup. CurrentProvider() resolves via sync.Once and freezes; tests inject via SetProviderForTest. resolveProvider is a pure function so the routing table can be exercised without env-var gymnastics.
  3. Normalize URLs in tokenmanager same-host / aud / cache-key compares. normalizeOriginURL handles trailing slash, scheme/host case, and default ports (RFC 3986 §6.2.2.1 / §6.2.3). Non-URL audiences pass through unchanged for byte-exact compare.
  4. Preflight core-token expiry and clear the exchange cache on SaveCoreToken. Long-expired tokens surface as ErrNotLoggedIn (so the "run login" UX kicks in) instead of confusing STS / 401 errors. A re-login can't return the previous user's exchanged tokens — defence-in-depth against future cache-key refactors.
  5. Tighten tokenstore malformed-JSON detection. Well-formed JSON without an access_token (e.g. {} or an unrelated CLI's blob) now surfaces as ErrMalformed. The shim's bare-string fallback rejects JSON-shaped content via looksLikeBareToken so Authorization: Bearer {} can't ship.
  6. Add per-request timeouts (DefaultRequestTimeout = 30s) to deviceflow.Client and sts.Client. Wrap lives at the method level so the deadline covers the body read, not just the dial. Tests pin both the firing path and the default/override resolution.

Test plan

🤖 Generated with Claude Code


Note

Medium Risk
Touches authentication/token acquisition and storage paths (provider routing, token caching/expiry, keyring decoding, and HTTP request deadlines), which can affect login/logout and API access if misconfigured. Changes are well-covered by new unit tests but still impact security-sensitive code paths.

Overview
Auth provider routing is centralized and made deterministic. Provider selection now lives in auth.CurrentProvider() (resolved once via sync.Once with a SetProviderForTest seam), and the provider table now also owns AuthTokensPath.

Auth-tokens API routing no longer reads env vars. api.Client gains WithAuthTokensPath, auth-tokens methods error if unset, and CLI call sites wire the path from auth.CurrentProvider().AuthTokensPath.

Token resolution is hardened. tokenmanager now normalizes origin URLs for same-host/audience checks and cache keys, preflights core-token JWT exp (returning ErrNotLoggedIn when expired), and clears the exchange cache on SaveCoreToken.

Token storage/IO is tightened and bounded. Keyring decoding treats well-formed JSON without access_token as ErrMalformed, the shim’s bare-token fallback rejects JSON-shaped blobs, and deviceflow.Client/sts.Client add a default 30s per-request timeout (overrideable/disableable) with tests pinning slow-loris behavior.

Reviewed by Cursor Bugbot for commit 6a9e601. Configure here.

…preflight, timeouts)

Six fixes called out across the security/architecture/correctness reviews
of #cli-auth-consolidation. Each fix lands with focused tests; no
behaviour changes outside the auth path.

1. Eliminate duplicate ENTIRE_AUTH_PROVIDER_VERSION read in
   cmd/entire/cli/api/auth_tokens.go. The provider table in
   cmd/entire/cli/auth.Provider now owns AuthTokensPath; api.Client
   takes it via WithAuthTokensPath. The api package no longer reads
   the env var.

2. Read provider version once at startup. CurrentProvider() resolves
   via sync.Once and freezes; tests inject via SetProviderForTest.
   resolveProvider is a pure function so the routing table is
   exercisable without env-var gymnastics.

3. Normalize URLs in tokenmanager same-host / aud / cache-key compares.
   normalizeOriginURL handles trailing slash, scheme/host case, and
   default ports (RFC 3986 §6.2.2.1 / §6.2.3); non-URL audiences pass
   through unchanged for byte-exact compare.

4. Preflight core-token expiry and clear the exchange cache on
   SaveCoreToken. Long-expired tokens surface as ErrNotLoggedIn (so
   "run login" UX kicks in) instead of confusing STS / 401 errors;
   a re-login can't return the previous user's exchanged tokens.

5. Tighten tokenstore malformed-JSON detection. Well-formed JSON
   without an access_token now surfaces as ErrMalformed. The shim's
   bare-string fallback rejects JSON-shaped content via
   looksLikeBareToken so "Authorization: Bearer {}" can't ship.

6. Add per-request timeouts (DefaultRequestTimeout = 30s) to
   deviceflow.Client and sts.Client via context.WithTimeout. The wrap
   lives at the method level so the deadline covers the body read,
   not just the dial. Tests pin both the firing path and the
   default/override resolution.

Entire-Checkpoint: c79c0ff7d6c1
Copilot AI review requested due to automatic review settings May 8, 2026 12:04
@Soph Soph requested a review from a team as a code owner May 8, 2026 12:04
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 PR applies a set of follow-up fixes to the CLI authentication path (provider routing, token-store hardening, tokenmanager correctness, and network robustness) intended to address review feedback from #1153 without impacting non-auth behavior.

Changes:

  • Centralizes provider-version routing (including auth-tokens endpoint base path) in cmd/entire/cli/auth and threads it into api.Client via WithAuthTokensPath, removing env-var reads from api/.
  • Hardens token resolution/storage behavior: URL normalization for equality/cache keys, core-token expiry preflight, exchange-cache invalidation on re-login, and stricter malformed-token detection (including legacy bare-string fallback filtering).
  • Adds per-request HTTP timeouts (default 30s, configurable) to RFC 8628 device-flow and RFC 8693 STS clients with focused tests.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
cmd/entire/cli/logout.go Routes logout token revocation through WithAuthTokensPath based on the active provider.
cmd/entire/cli/auth.go Updates auth status/list/revoke flows to use provider-supplied auth-tokens base path.
cmd/entire/cli/auth/store.go Adds JSON-shaped filtering to legacy bare-token fallback reads.
cmd/entire/cli/auth/store_test.go Adds tests ensuring JSON-shaped values aren’t shipped as bearer tokens.
cmd/entire/cli/auth/provider.go Introduces exported Provider, pure resolveProvider, and a cached CurrentProvider with test override.
cmd/entire/cli/auth/provider_test.go Tests provider resolution, trimming, and test override seam.
cmd/entire/cli/auth/exchange.go Switches manager construction to CurrentProvider() (single env read per process).
cmd/entire/cli/auth/client.go Switches device-flow client wiring to CurrentProvider() fields.
cmd/entire/cli/api/client.go Adds authTokensPath field and WithAuthTokensPath configuration hook.
cmd/entire/cli/api/auth_tokens.go Removes provider env-var routing; errors clearly when auth-tokens path is unset.
cmd/entire/cli/api/auth_tokens_test.go Refactors tests to configure auth-tokens path explicitly and asserts unset-path errors.
auth/tokenstore/keyring.go Treats well-formed JSON without access_token as ErrMalformed.
auth/tokenstore/keyring_test.go Adds coverage for empty/missing access_token malformed cases.
auth/tokenmanager/tokenmanager.go Adds core-token expiry preflight, URL normalization for comparisons/cache keys, and clears exchange cache on core-token save.
auth/tokenmanager/tokenmanager_test.go Adds tests for expiry preflight, URL normalization behavior, and cache invalidation on SaveCoreToken.
auth/sts/sts.go Adds per-request timeout policy to STS exchanges (default 30s, configurable/disableable).
auth/sts/sts_test.go Adds slow-loris/timeout tests and timeout-resolution unit tests.
auth/deviceflow/deviceflow.go Adds per-request timeout policy to device-flow requests (default 30s, configurable/disableable).
auth/deviceflow/deviceflow_test.go Adds slow-loris/timeout tests and timeout-resolution unit tests.

Comment on lines +195 to +203
got, err := NewStoreWithService(service).LoadTokens(profile)
// We expect ErrNotFound — JSON-shaped malformed entries must
// not be routed through the bare-string fallback.
if err == nil {
t.Fatalf("LoadTokens(%q) returned %+v; want ErrNotFound", body, got)
}
if got.AccessToken != "" {
t.Fatalf("LoadTokens(%q) AccessToken = %q, want empty", body, got.AccessToken)
}
Comment on lines +315 to +323
// normalizeOriginURL canonicalises an origin URL for equality
// comparisons. RFC 3986 §6.2.2.1 makes scheme and host case-insensitive
// and §6.2.3 makes the empty path equivalent to "/" — we collapse to
// no-trailing-slash. Default ports (80/http, 443/https) are stripped.
//
// On parse failure (or when the input lacks a scheme or host — common
// for non-URL audiences) the input is returned unchanged so callers
// fall back to byte-exact comparison.
func normalizeOriginURL(raw string) string {
@khaong
Copy link
Copy Markdown
Contributor

khaong commented May 9, 2026

@cursor review

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.

✅ 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 6a9e601. Configure here.

// it, ParseClaims is documented as unverified.
func makeJWTWithExp(t *testing.T, exp time.Time, aud []string) string {
t.Helper()
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should always reject alg:none and have a test for that to avoid future regression. For other tests, we should be able to produce a completely valid token with specific issues - e.g. expired token.

@khaong khaong merged commit dc7c003 into alex/cli-auth-consolidation May 14, 2026
14 checks passed
@khaong khaong deleted the alex/cli-auth-followup-fixes branch May 14, 2026 06:33
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.

4 participants