Skip to content

feat(cli): sign in with xping login in the browser, and add logout and auth status (P3) - #247

Merged
xping-admin merged 4 commits into
feat/cli-authfrom
feat/cli-auth-03-loopback-login
Oct 6, 2026
Merged

xping-admin merged 4 commits into
feat/cli-authfrom
feat/cli-auth-03-loopback-login

Conversation

@xping-admin

@xping-admin xping-admin commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

Phase 3 of docs/internals/implementation-specs/cli-auth-cli-spec.md (§19). Merges into the feat/cli-auth integration branch, not main.

Implements contract §3.1 (loopback flow), §3.4 (logout), §6.1 (claims for display), §10.1/§10.2 (loopback security) and §10.4 (storage at login), and spec §3.2–§3.4, §4, §5 and §12.

Spec amendments (committed before the code)

  • §3.1: the auth commands' decoration follows whether stderr is a terminal. ConsoleIO now has one terminal flag per stream.
  • §3.4: credentialSource gains "config" for an Xping:ApiKey from appsettings.
  • §18.2: flow tests always inject the environment, because CI runners set CI=true.
  • §4.3: the [::1] fallback binds on Windows only. The managed HttpListener on macOS and Linux rejects bracketed IPv6 prefixes ("Invalid port in prefix"). The attempt is kept so the retry order is the same on every OS.

What changes

  • Pkce: 32-byte verifier and state, S256 challenge, constant-time state compare, bytes zeroed on dispose.
  • LoopbackListener + LoopbackPages:
    • Probes a port, then binds 127.0.0.1 up to 3 times, then [::1].
    • Answers per the §4.4 table: 404 for stray requests, 400 for a wrong state, then keeps waiting (A-6).
    • Sets the safe headers. Pages never echo the code, state, URL, or error_description.
  • IHeadlessDetector / HeadlessDetector (§5.2) and IBrowserLauncher / BrowserLauncher (§5.1): per-OS commands, the URL passed as one argument, a 2 s cap, never throws.
  • LoopbackFlow:
    • Fresh discovery and the authorization URL.
    • A 5-minute timeout on TimeProvider.
    • The code exchange, with error mapping per §4.8.
    • The CredentialRecord: expiry is receipt time + expires_in; claims come from AccessTokenClaims.
  • LoginCommand:
    • Refuses without a TTY on stdin/stderr or with a truthy CI (exit 13).
    • Stores through CredentialStores.
    • Prints the success block. Under --json, writes a document for every outcome, including cancelled.
  • LogoutCommand (§12): revokes with a 15 s cap, then removes the sign-in from every store, plus the project and discovery caches. Online gives signed-out, offline signed-out-locally with a warning, and no sign-in gives not-signed-in.
  • AuthStatusCommand (§3.4): text and JSON for every credential state, exit 0 / 10 / 17, and no request ever. --verbose lists the ~/.xping paths.
  • AuthJson (its own options, nulls written; the precedent is ReportJsonOptions), AuthText (stderr rendering), CliConfigurationLoader, ProcessEnvironment (environment seam).

Tests

  • Flow tests run Program.Run in-process against FakeCloud, which gains /connect/authorize with Approve / Deny / WrongState / ServerError / NoRedirect scripts. A FakeBrowser follows the redirect to the real listener.
    • Login_Loopback_Success, _Denied, _Timeout, _StateMismatch_ThenGenuine, _StateMismatch_Only
    • Login_Discovery_IssuerMismatch, _ContractVersion, _MinCliVersion
    • Login_NoTty, Login_CiSet, Login_WithApiKeySet_Proceeds
    • Logout_Revokes_ThenDeletes, Logout_Offline_DeletesAndWarns, Logout_NotSignedIn, Logout_ApiKeyOnly
    • AuthStatus_*, each asserting that no request was made
  • Unit tests: PkceTests (RFC 7636 appendix B vector), LoopbackListenerTests (real listener; bind retry against a held port), HeadlessDetectorTests (every §5.2 row), BrowserLauncherTests, AccessTokenClaimsTests, AuthJsonTests.
  • Xping.Cli.Tests: 1937 passed. Release build has no warnings; dotnet format --verify-no-changes is clean.

Manual verification against production (exit criterion)

On macOS with the file store:

  • xping login: the browser opened, the link was printed once, and the success block showed the workspace, the session suffix, and Stored in ~/.xping/credentials.json.
  • xping auth status: stored login, access token "expires in 14 minutes".
  • ~/.xping/credentials.json is -rw-------.
  • xping logout: "Signed out of https://app.xping.io (…)". A second logout says "You are not signed in".

Verified: Windows, non-elevated. The loopback listener uses HttpListener (http.sys) on http://127.0.0.1:{port}/. The review asked whether http.sys lets a standard, non-admin process register that prefix without a URL reservation. If it doesn't, every Windows login fails with "Could not open a local port", and the spec (§4.3) forbids a localhost fallback. Checked on a Windows laptop from a regular, non-elevated cmd: http.sys accepts the 127.0.0.1 prefix without a URL reservation, and everything behaved as on macOS.

  • dotnet test tests/Xping.Cli.Tests -c Release --filter "FullyQualifiedName~LoopbackListenerTests" passes
  • xping login succeeds against production; auth status shows the login; logout revokes

Review fixes (f293044)

  • AuthCommandRunner is shared by the three commands. A bad XPING_CLOUDURL or settings file now writes the failure document under --json (error: configuration, exit 2). The document names the configured Cloud URL, and every failure message is scrubbed on both streams.
  • Local listener failures report local_listener, not oauth_error.
  • logout now says the server refused, rather than that it could not be reached, when revocation fails.
  • An invalid_request without a description no longer prints a stray ": .".
  • Launchers run with /dev/null streams through a constant sh script on Unix, with the URL passed as an argument. A browser they start can neither write into --json output nor die on a closed pipe.
  • Spec §3.2 and §5.1 are amended. AGENTS.md now names the --json output as the exception to the IXpingSerializer rule.

Not in this phase: --device, --no-browser and --workspace (P4); keychains and the fallback warning (P5); refresh and the bearer pipeline (P6); docs/cli/command-reference.md (P8).

🤖 Generated with Claude Code

xping-admin and others added 3 commits October 6, 2026 15:08
…fig key source, flow test seams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The managed HttpListener of macOS and Linux rejects bracketed IPv6
prefixes, so the §4.3 fallback to [::1] can only bind under http.sys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d auth status (P3)

xping login runs the loopback PKCE flow (cli-auth-cli-spec §4): fresh
discovery, a one-shot listener on 127.0.0.1, the link printed once, the
browser opened unless the machine is headless, a 5-minute wait, the
code exchange, and the record stored in the file store. It refuses to
run without a terminal or with CI set, and every failure has its exit
code and, under --json, its JSON document.

xping logout revokes the refresh token and removes the sign-in and the
caches for the Cloud URL, locally even when revocation fails (§12).
xping auth status reports the credential in use without a request
(§3.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.22581% with 99 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Xping.Cli/Auth/Browser/BrowserLauncher.cs 63.07% 20 Missing and 4 partials ⚠️
src/Xping.Cli/Commands/Auth/LogoutCommand.cs 78.31% 16 Missing and 2 partials ⚠️
src/Xping.Cli/Commands/Auth/AuthStatusCommand.cs 85.41% 6 Missing and 8 partials ⚠️
src/Xping.Cli/Commands/Auth/LoginCommand.cs 80.82% 13 Missing and 1 partial ⚠️
src/Xping.Cli/Auth/Loopback/LoopbackListener.cs 88.46% 11 Missing and 1 partial ⚠️
src/Xping.Cli/Auth/Flows/LoopbackFlow.cs 92.56% 5 Missing and 4 partials ⚠️
src/Xping.Cli/Auth/Loopback/Pkce.cs 90.00% 1 Missing and 2 partials ⚠️
src/Xping.Cli/Commands/Auth/AuthJson.cs 96.55% 1 Missing and 1 partial ⚠️
src/Xping.Cli/Commands/Auth/AuthText.cs 91.30% 0 Missing and 2 partials ⚠️
src/Xping.Cli/Auth/Browser/HostOs.cs 80.00% 0 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
src/Xping.Cli/Auth/AccessTokenClaims.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/AuthFailureException.cs 73.91% <100.00%> (ø)
src/Xping.Cli/Auth/Browser/HeadlessDetector.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/Discovery/DiscoveryCache.cs 89.74% <100.00%> (ø)
src/Xping.Cli/Auth/Loopback/LoopbackPages.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Auth/XpingHome.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Commands/Auth/AuthCommandRunner.cs 100.00% <100.00%> (ø)
.../Xping.Cli/Configuration/CliConfigurationLoader.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Hosting/ConsoleIO.cs 100.00% <100.00%> (ø)
src/Xping.Cli/Hosting/ProcessEnvironment.cs 100.00% <100.00%> (ø)
... and 12 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…configured Cloud URL (#247 review)

- One AuthCommandRunner for login, logout and auth status: a bad
  XPING_CLOUDURL or settings file now writes the failure document
  (error "configuration", exit 2), the document names the configured
  Cloud URL, and every failure message is scrubbed on both streams.
- Local listener failures report "local_listener", not "oauth_error".
- logout tells "refused" from "could not reach" when revocation fails.
- An invalid_request without a description reads cleanly.
- Browser launchers run with /dev/null streams through a constant sh
  script, so a browser they start cannot write into --json output or
  die on a closed pipe. The launcher's duplicate headless check is gone.
- Spec §3.2 and §5.1 amended; AGENTS.md names the --json output as the
  exception to the IXpingSerializer rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@xping-admin
xping-admin merged commit 5ffcb8a into feat/cli-auth Oct 6, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant