Skip to content

feat(oauth): reusable provider OAuth engine + zero auth CLI#210

Merged
gnanam1990 merged 6 commits into
mainfrom
feat/oauth-module
Jun 15, 2026
Merged

feat(oauth): reusable provider OAuth engine + zero auth CLI#210
gnanam1990 merged 6 commits into
mainfrom
feat/oauth-module

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds internal/oauth — a transport/identity-agnostic OAuth 2.0 engine — plus a zero auth CLI for logging in to model providers, alongside the MCP-server OAuth zero already has. Additive and provider-neutral: no providers, endpoints, or client identities are baked into the binary. Every provider is configured entirely via ZERO_OAUTH_<NAME>_* env vars, so the engine works with any OAuth 2.0 / OIDC server and ships no third-party brand or client identity.

Engine (internal/oauth)

  • PKCE — S256 mandatory (plain refused) + per-flow CSRF state.
  • Auth-code flowBuildAuthorizationURL / ExchangeCode / Refresh, with an https-only token-endpoint guard (loopback exempt) and errors that carry only error/error_description — never token material.
  • Loopback callback — 127.0.0.1-only, OS-assigned port, single use, state verified, then closed.
  • Device grant (RFC 8628)RequestDeviceCode + PollDeviceToken honoring authorization_pending / slow_down / interval / expiry (headless/SSH).
  • Discovery — RFC 8414 + OIDC openid-configuration fallback (issuer-driven).
  • Token store — namespaced keys (provider:<name> / mcp:<name>), 0600, file-locked (ownership-aware), atomic writes, malformed-fails-closed; GetFresh (on-demand refresh) + Handle401 (forced).
  • RefreshScheduler — opt-in proactive refresh before expiry.

CLI (zero auth)

login <provider> [--device] [--scope …] · logout <provider> · status [provider] · refresh <provider> [--watch]. Status/output never print token material. Wired into the dispatch + top-level help.

A provider named <name> is configured via: ZERO_OAUTH_<NAME>_CLIENT_ID (required), _CLIENT_SECRET, _AUTHORIZE_URL, _TOKEN_URL, _DEVICE_URL, _ISSUER_URL, _SCOPES, _FLOW (loopback|device). Endpoint URLs must be https (loopback exempt).

Security

PKCE-S256 mandatory · CSRF state verified · loopback-only single-use callback · https-only token endpoint · token material never logged (error bodies redacted to error/error_description) · token files 0600 + locked · provider login is opt-in and inert until zero auth login is run. No new dependencies (stdlib only).

Not touched / deferred

  • MCP OAuth (internal/mcp) is byte-for-byte unchanged — zero regression. Migrating MCP to share this engine is a documented, test-guarded follow-up.
  • Encrypted-at-rest / OS-keyring storage backends and the enterprise cloud-credential providers are explicit follow-ups (the keyring + cloud SDKs would add deps).

Verification

Tests cover every path (PKCE, auth-URL, https-guard + redaction, exchange/refresh, loopback capture + state-mismatch + timeout, device pending/slow_down/expired, store namespacing/0600/locking, provider env resolution, manager login loopback+device / GetFresh / Handle401, scheduler).

Gates: gofmt · vet · build (host+linux+windows) · test -race · staticcheck (no new) · govulncheck (0) · deadcode (no new) all pass.

Local run check (built binary): auth --help, status, login validation, unconfigured-provider error, logout, and mcp oauth --help (no regression) all verified; status confirmed to leak no token material. A live login completes against an in-process httptest server in the unit tests (the sandbox blocks cross-process loopback, so the standalone-binary↔fake-server E2E can't run in CI).

Summary by CodeRabbit

New Features

  • Added a top-level zero auth CLI command with login, logout, status, and refresh, including updated --help, interactive loopback or device-code login, provider-scoped status, repeatable --scope, and --json output with secret-safe redaction.
  • Introduced a full OAuth implementation with RFC 8414 discovery, S256 PKCE authorization URLs, secure token exchange/refresh (including proactive refresh scheduling), a file-backed token store, and loopback/device authorization helpers.

Tests

  • Added CLI, unit, and integration tests covering auth command validation/outputs, discovery/config, refresh scheduling, token persistence, polling behavior, and redaction/flag handling.

Add internal/oauth: a transport/identity-agnostic OAuth 2.0 engine that powers
provider login alongside the MCP-server OAuth zero already has. It is additive
and provider-neutral — NO providers, endpoints, or client identities are baked
into the binary; every provider is configured entirely via ZERO_OAUTH_<NAME>_*
env vars, so the engine works with any OAuth 2.0 / OIDC server.

Engine (internal/oauth):
- PKCE (S256 mandatory; "plain" refused) + per-flow CSRF state.
- Authorization-code flow: BuildAuthorizationURL / ExchangeCode / Refresh, with
  an https-only token-endpoint guard (loopback exempt) and error bodies that
  carry only error/error_description (never token material).
- Loopback callback server: 127.0.0.1-only, OS-assigned port, single use, state
  verified, then closed.
- Device-authorization grant (RFC 8628): RequestDeviceCode + PollDeviceToken
  honoring authorization_pending / slow_down / interval / expiry (headless/SSH).
- RFC 8414 + OIDC discovery (issuer-driven endpoint resolution).
- Namespaced token store ("provider:<name>" / "mcp:<name>"), 0600, file-locked
  (ownership-aware), atomic writes, malformed-fails-closed; GetFresh (on-demand
  refresh) + Handle401 (forced refresh).
- Opt-in proactive RefreshScheduler.

CLI (zero auth):
- login <provider> [--device] [--scope ...], logout, status [provider],
  refresh <provider> [--watch]. Status/output never print token material.
- Registered in app.go dispatch + top-level help.

MCP OAuth (internal/mcp) is left byte-for-byte unchanged (zero regression);
sharing the engine with MCP and encrypted-at-rest/keyring storage are documented
follow-ups. Tests cover every path (PKCE, auth URL, https-guard + redaction,
exchange/refresh, loopback capture + state-mismatch + timeout, device pending/
slow_down/expired, store namespacing/0600/locking, provider env resolution,
manager login (loopback+device)/GetFresh/Handle401, scheduler). Gates: gofmt /
vet / build (host+linux+windows) / test -race / staticcheck (no new) /
govulncheck (0) / deadcode (no new) all pass.
@github-actions

github-actions Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 474a40a70a56
Changed files (24): internal/cli/app.go, internal/cli/auth.go, internal/cli/auth_test.go, internal/mcp/oauth.go, internal/oauth/device.go, internal/oauth/device_test.go, internal/oauth/discovery.go, internal/oauth/flow.go, internal/oauth/flow_test.go, internal/oauth/lock.go, internal/oauth/loopback.go, internal/oauth/loopback_test.go, and 12 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 102544e1-258e-47de-8c87-b826f30d4426

📥 Commits

Reviewing files that changed from the base of the PR and between ac0c659 and 474a40a.

📒 Files selected for processing (1)
  • internal/mcp/oauth.go

Walkthrough

Adds a complete internal/oauth package implementing OAuth 2.0 authorization-code (PKCE + loopback), RFC 8628 device-code, RFC 8414 discovery, file-backed token store with cross-process locking, a proactive refresh scheduler, and an env-driven provider registry. Wires everything into a new zero auth CLI command with login, logout, status, and refresh (including --watch) subcommands. Refactors internal/mcp/oauth.go to delegate to the shared engine. Minor performance improvement to mailbox lock retry latency.

Changes

OAuth Engine and CLI Integration

Layer / File(s) Summary
Core types, errors, PKCE, and state
internal/oauth/oauth.go, internal/oauth/pkce.go
Token, Config, six exported error sentinels, PKCE struct, NewPKCE() (S256 only), and NewState() are introduced as shared contracts; PKCE/state generation validate cryptographic entropy and S256 challenge derivation.
Token store and cross-process file lock
internal/oauth/store.go, internal/oauth/lock.go
File-backed 0600 JSON store with XDG/env path resolution, schema versioning, atomic rename-based writes, key-namespace validation; acquireFileLock exclusive-lock helper with stale-lock detection and ownership-safe release; tests verify save/load/delete, key validation, status filtering, file mode, malformed-JSON fail-closed, and path override.
Authorization-code + PKCE flow and token endpoint validation
internal/oauth/flow.go
BuildAuthorizationURL, ExchangeCode, Refresh, PostToken enforce PKCE S256, require HTTPS on token endpoints (except loopback), redact error details, parse/merge token fields; tests validate PKCE parameters, reserved-field protection, endpoint security, and error redaction.
Loopback HTTP redirect listener
internal/oauth/loopback.go
Single-use HTTP listener bound to 127.0.0.1:ephemeral-port, validates CSRF state, extracts authorization code, supports context cancellation and graceful shutdown; tests verify code capture, state mismatch rejection, timeout handling, and provider error propagation.
RFC 8628 device-code authorization
internal/oauth/device.go
RequestDeviceCode (POST /device_authorization, user/device code extraction, interval defaulting, expiry computation) and PollDeviceToken polling loop (honors authorization_pending and slow_down, respects expiry deadline, context cancellation); tests verify interval defaults, pending/slow-down retry behavior, expiry enforcement, and access_denied errors.
RFC 8414 authorization-server metadata discovery
internal/oauth/discovery.go
DiscoverAuthorizationServer constructs well-known metadata URL, performs HTTP GET with size limits and JSON decoding; joinWellKnown validates base URL, inserts /.well-known segment, preserves path-based issuer tenant segments, clears query/fragment.
Environment-driven provider registry
internal/oauth/providers.go
Flow type (loopback/device constants), ValidateProviderName regex, Registry.ResolveConfig reads ZERO_OAUTH_<NAME>_* env vars, validates required fields, selects flow, enforces HTTPS; tests cover config resolution, flow selection, missing-field errors, insecure-endpoint rejection, and provider-name validation.
Manager orchestration and token refresh
internal/oauth/manager.go
Manager orchestrates login (endpoint discovery, loopback vs device), GetFresh/Handle401 conditional/forced refresh, logout, and status queries; tests cover loopback/device login, expired-token refresh, valid-token skip, forced 401 refresh, and idempotent logout.
Proactive token refresh scheduler
internal/oauth/scheduler.go
RefreshScheduler background goroutine reloads token, waits until pre-expiry (with deterministic jitter), calls Manager.GetFresh proactively; handles load failures with backoff/termination limits; idempotent Start/Stop; tests verify no-op when refresh token absent and actual refresh when nearing expiry.
zero auth CLI wiring and tests
internal/cli/app.go, internal/cli/auth.go
Registers auth in app dispatch and help text; implements runAuth dispatcher, parseAuthArgs shared flag validation, newAuthManager factory, and subcommand handlers (login with --device/--scope, logout with --json, status with provider filter, refresh with --watch scheduling); CLI tests verify validation, secret non-leakage, help text, and error paths.
MCP OAuth delegation to shared engine
internal/mcp/oauth.go
Refactors to delegate PKCE generation, discovery, authorization URL construction, code exchange, and token refresh to internal/oauth functions; removes local implementations and simplifies imports.

Mailbox Lock Retry Latency Improvement

Layer / File(s) Summary
Reduced lock sleep and test timeout
internal/swarm/mailbox.go, internal/swarm/mailbox_test.go
Decreases acquireLock retry sleep from 20ms to 2ms for faster re-acquisition under contention; increases TestMailboxConcurrentSends timeout from 10s to 60s to accommodate heavier concurrent load in slower CI environments.

Sequence Diagram(s)

sequenceDiagram
  participant CLI as zero auth login
  participant Manager
  participant Registry
  participant Discovery as DiscoverAuthorizationServer
  participant LoopbackListener
  participant AuthServer
  participant Store

  CLI->>Manager: Login(ctx, LoginOptions{Provider, Device, Scopes})
  Manager->>Registry: ResolveConfig(name, env)
  Registry-->>Manager: Config, Flow
  Manager->>Discovery: DiscoverAuthorizationServer(issuerURL)
  Discovery-->>Manager: ServerMetadata (endpoints)
  alt Flow == loopback
    Manager->>LoopbackListener: NewLoopbackListener(state)
    Manager->>AuthServer: open BuildAuthorizationURL(cfg, pkce, state, redirectURI)
    AuthServer->>LoopbackListener: GET /callback?code=&state=
    LoopbackListener-->>Manager: authorization code
    Manager->>AuthServer: ExchangeCode POST /token
    AuthServer-->>Manager: Token
  else Flow == device
    Manager->>AuthServer: RequestDeviceCode POST /device_authorization
    AuthServer-->>Manager: DeviceAuth (user_code, verification_uri)
    Manager->>CLI: print user instructions
    Manager->>AuthServer: PollDeviceToken (repeat until success)
    AuthServer-->>Manager: Token
  end
  Manager->>Store: Save("provider:<name>", Token)
  Manager-->>CLI: Status
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Gitlawb/zero#54: Touches internal/cli/app.go command dispatch to add/route top-level CLI subcommands.
  • Gitlawb/zero#203: Modifies internal/cli/app.go's dispatch and help to register new subcommands.
  • Gitlawb/zero#179: Refactors internal/mcp/oauth.go to delegate to the shared internal/oauth engine built in this PR.

Suggested reviewers

  • Vasanthdev2004
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title precisely captures the main changes: introducing a reusable OAuth engine in internal/oauth and a new zero auth CLI for provider authentication.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oauth-module

Comment @coderabbitai help to get the list of available commands and usage tips.

TestResolveStorePathHonorsOverride hardcoded a unix "/tmp/custom/tok.json"
literal, which is not absolute on Windows (no drive), so ResolveStorePath
correctly resolves it against the current drive and the verbatim comparison
failed (D:\tmp\custom\tok.json). The production code is correct; the test now
builds the override with filepath.Join(t.TempDir(), ...) so it is absolute and
clean on every OS.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (2)
internal/cli/auth_test.go (1)

66-99: ⚡ Quick win

Add regression tests for unsupported flag rejection and empty scope validation.

Current tests miss key parser/handler edge cases (e.g., status --watch, logout --device, refresh --json, and login --scope ""). Adding these will lock in the CLI contract and prevent silent-flag regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/cli/auth_test.go` around lines 66 - 99, The current test coverage is
missing key parser and handler edge cases that should reject unsupported flags.
Add new regression test functions following the same pattern as
TestRunAuthLoginValidation, TestRunAuthLoginUnknownProvider, and
TestRunAuthRefreshNoToken to cover the following scenarios: status command with
--watch flag should fail, logout command with --device flag should fail, refresh
command with --json flag should fail, and login command with an empty --scope
value should fail. Each test should invoke runWithDeps with the respective
command and flags, verify that exitSuccess is not returned, and check stderr for
appropriate error messages when applicable.
internal/oauth/flow_test.go (1)

48-78: ⚡ Quick win

Add a regression test for reserved OAuth/PKCE parameter override.

Line 48-78 verifies additive params, but not that extraParams cannot override reserved keys (state, redirect_uri, code_challenge, code_challenge_method). Add a case that attempts override (e.g., "code_challenge_method":"plain") and assert the final query still keeps the enforced values.

Proposed test shape
+func TestBuildAuthorizationURLReservedParamsCannotBeOverridden(t *testing.T) {
+	cfg := Config{
+		ClientID:              "client-123",
+		AuthorizationEndpoint: "https://auth.example.com/authorize",
+		ExtraAuthParams:       map[string]string{"code_challenge_method": "plain"},
+	}
+	pkce := PKCE{Verifier: "v", Challenge: "chal", Method: MethodS256}
+	raw, err := BuildAuthorizationURL(
+		cfg,
+		pkce,
+		"state-xyz",
+		"http://127.0.0.1:9/callback",
+		map[string]string{"state": "attacker-state"},
+	)
+	if err != nil {
+		t.Fatalf("BuildAuthorizationURL: %v", err)
+	}
+	u, _ := url.Parse(raw)
+	q := u.Query()
+	if got := q.Get("code_challenge_method"); got != MethodS256 {
+		t.Fatalf("code_challenge_method = %q, want %q", got, MethodS256)
+	}
+	if got := q.Get("state"); got != "state-xyz" {
+		t.Fatalf("state = %q, want state-xyz", got)
+	}
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/flow_test.go` around lines 48 - 78, The test
TestBuildAuthorizationURL verifies that extra parameters are correctly added to
the authorization URL, but does not test that reserved OAuth/PKCE parameters
(such as code_challenge_method, state, redirect_uri, code_challenge) cannot be
overridden through the extraParams argument. Add a new test case that passes
extraParams containing an attempted override of a reserved parameter (for
example, passing "code_challenge_method" set to "plain" when it should be
"S256") and assert that the resulting query URL retains the correct enforced
value for that reserved parameter, demonstrating that the override attempt was
properly rejected.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/auth.go`:
- Around line 52-80: The parseAuthArgs function currently accepts all flags
unconditionally and does not validate scope values, allowing empty scopes and
flags that some subcommands do not support to pass through silently. Add
validation to reject empty scope values by checking if the trimmed scope string
is empty and returning an error, and refactor the function to enforce
subcommand-specific flag validation so that only flags relevant to the specific
subcommand are accepted (this will require the function to know which subcommand
is being parsed, or alternatively validate flags after parsing based on the
subcommand context). Additionally, review and apply the same validation logic to
all other flag parsing locations in the auth module (referenced at lines
102-242) to ensure consistent behavior across all subcommands.

In `@internal/oauth/device_test.go`:
- Around line 87-88: The DeviceAuth setup in the slow_down test is creating an
expiration window that is too short relative to the delay introduced by the
slow_down mechanism itself. The ExpiresAt field is set to 5 seconds in the
future, but the slow_down test adds a 5-second wait after the first poll,
causing the token to expire before the success path can be validated. Increase
the ExpiresAt duration in the DeviceAuth struct initialization to provide
sufficient time for the slow_down delay to complete before expiration, ensuring
the test validates the success path under normal (non-expired) conditions rather
than relying on post-expiry polling behavior.

In `@internal/oauth/device.go`:
- Around line 81-97: Fail closed on missing device authorization expiry instead
of silently accepting it. In the device authorization response handling around
lines 81-97, change the logic to return an error when parsed.ExpiresIn is not
greater than 0, rather than allowing ExpiresAt to remain zero which disables
expiry checking. Additionally, fix the polling loop at lines 113-122 to check if
the current time has reached or passed ExpiresAt before attempting to poll,
preventing any additional poll attempts after the device authorization expires,
particularly after slow_down adjustments.

In `@internal/oauth/flow.go`:
- Around line 46-60: The query parameters for extra auth params and extra params
are being set after the critical OAuth and PKCE fields (state, redirect_uri,
code_challenge, code_challenge_method), which allows callers to override these
security-critical values. Reorder the code so that the loops iterating over
cfg.ExtraAuthParams and extraParams are executed first, and then set the
required/enforced fields (response_type, client_id, redirect_uri, state,
code_challenge, code_challenge_method, and scope) afterwards. This ensures that
the critical security parameters cannot be overridden by caller-provided values.

In `@internal/oauth/lock.go`:
- Around line 36-37: The WriteString and Close operations on the file variable f
are ignoring errors using blank identifiers, which can leave a lock file in a
corrupted or partial state if these operations fail. Instead of ignoring these
errors with _, capture the errors returned by f.WriteString(token) and
f.Close(), check them appropriately, and return an error if either operation
fails before proceeding to return success. This ensures the lock file is only
considered valid if both the token write and file close complete successfully.

In `@internal/oauth/manager.go`:
- Around line 138-142: The nilerr lint error occurs because the code returns nil
from a branch where err is non-nil (in the error handler after m.discover call).
Restructure the code to avoid returning when the discovery fails: invert the
condition to check if err is nil instead, process the meta only when discovery
succeeds, and allow the function to continue normally (returning cfg, nil at the
end) when discovery fails. This keeps the non-fatal fallback behavior where
pinned endpoints are used when discovery is unsuccessful, while satisfying the
golangci-lint nilerr rule that forbids returning nil from branches with non-nil
errors.

In `@internal/oauth/providers.go`:
- Around line 76-84: The line `flow := FlowLoopback` before the switch statement
is flagged as an ineffectual assignment because every code path in the switch
statement either assigns a value to the flow variable or returns an error.
Remove the initial `flow := FlowLoopback` assignment, as all successful switch
cases (including the empty string and string(FlowLoopback) cases) will assign
the appropriate value before the variable is used.

In `@internal/oauth/scheduler_test.go`:
- Around line 50-58: The test is ignoring errors returned by the token-store
operations in the NewRefreshScheduler test. In the store.Save() call for saving
the old token and the store.Load() call in the polling loop, replace the
underscore error ignoring with explicit error assertions. Use a testing
assertion method (such as require.NoError) to validate that both Save and Load
operations complete successfully, ensuring that token-store failures are caught
immediately rather than causing flaky timeout-based test failures.

In `@internal/oauth/scheduler.go`:
- Around line 48-52: The condition at line 50 in the scheduler.go file combines
error checking with permanent condition checks, causing any loadForKey error to
permanently terminate the scheduler. Instead, separate the error handling from
the permanent condition checks: only return when the permanent conditions are
met (token.RefreshToken is empty or token.ExpiresAt.IsZero()), and handle
loadForKey errors separately by implementing a backoff/retry mechanism instead
of immediately returning.

In `@internal/oauth/store_test.go`:
- Around line 104-110: In the TestResolveStorePathHonorsOverride function,
replace the hard-coded Unix path "/tmp/custom/tok.json" with a platform-native
path built using t.TempDir(). Call t.TempDir() to get a temporary directory
suitable for the current OS, then construct the override path by joining this
directory with a test file name (e.g., "tok.json"), and use the resulting path
both when passing the ZERO_OAUTH_TOKENS_PATH override to ResolveStorePath and in
the assertion that verifies the returned path matches the override. This ensures
the test remains portable across Windows and Unix-like systems.

In `@internal/oauth/store.go`:
- Around line 306-312: The envValue function in internal/oauth/store.go
currently falls back to os.Getenv whenever a key is missing from the env map,
even when a non-nil map is explicitly provided, which breaks test hermeticity.
Modify the function to treat non-nil env maps as authoritative: only call
os.Getenv as a fallback when the env map itself is nil. When env is non-nil but
lacks the requested key, return an empty string instead of falling back to the
process environment.

---

Nitpick comments:
In `@internal/cli/auth_test.go`:
- Around line 66-99: The current test coverage is missing key parser and handler
edge cases that should reject unsupported flags. Add new regression test
functions following the same pattern as TestRunAuthLoginValidation,
TestRunAuthLoginUnknownProvider, and TestRunAuthRefreshNoToken to cover the
following scenarios: status command with --watch flag should fail, logout
command with --device flag should fail, refresh command with --json flag should
fail, and login command with an empty --scope value should fail. Each test
should invoke runWithDeps with the respective command and flags, verify that
exitSuccess is not returned, and check stderr for appropriate error messages
when applicable.

In `@internal/oauth/flow_test.go`:
- Around line 48-78: The test TestBuildAuthorizationURL verifies that extra
parameters are correctly added to the authorization URL, but does not test that
reserved OAuth/PKCE parameters (such as code_challenge_method, state,
redirect_uri, code_challenge) cannot be overridden through the extraParams
argument. Add a new test case that passes extraParams containing an attempted
override of a reserved parameter (for example, passing "code_challenge_method"
set to "plain" when it should be "S256") and assert that the resulting query URL
retains the correct enforced value for that reserved parameter, demonstrating
that the override attempt was properly rejected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c8ddf7ec-b97b-4755-9f95-07b039134adb

📥 Commits

Reviewing files that changed from the base of the PR and between 9ddef0e and 5e70fac.

📒 Files selected for processing (21)
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/oauth/device.go
  • internal/oauth/device_test.go
  • internal/oauth/discovery.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/lock.go
  • internal/oauth/loopback.go
  • internal/oauth/loopback_test.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go
  • internal/oauth/oauth.go
  • internal/oauth/pkce.go
  • internal/oauth/providers.go
  • internal/oauth/providers_test.go
  • internal/oauth/scheduler.go
  • internal/oauth/scheduler_test.go
  • internal/oauth/store.go
  • internal/oauth/store_test.go

Comment thread internal/cli/auth.go Outdated
Comment thread internal/oauth/device_test.go Outdated
Comment thread internal/oauth/device.go
Comment thread internal/oauth/flow.go
Comment thread internal/oauth/lock.go Outdated
Comment thread internal/oauth/providers.go Outdated
Comment thread internal/oauth/scheduler_test.go Outdated
Comment thread internal/oauth/scheduler.go
Comment thread internal/oauth/store_test.go Outdated
Comment thread internal/oauth/store.go
Security / correctness:
- flow: caller-supplied ExtraAuthParams/extraParams can no longer override the
  reserved OAuth/PKCE fields (state, redirect_uri, code_challenge[_method],
  response_type, client_id) — e.g. forcing method=plain.
- device: a device-authorization response without expires_in now gets a bounded
  default lifetime (fail closed), and the poll loop re-checks expiry AFTER the
  interval sleep so it never polls past the deadline (e.g. after slow_down).
- lock: handle WriteString/Close errors on the token lock file — a partial write
  no longer strands an undeletable lock.
- store: a non-nil env map is now authoritative (hermetic) — a controlled map
  never falls back to ambient ZERO_OAUTH_* / HOME / XDG_CONFIG_HOME.
- scheduler: a transient loadForKey error backs off and retries (bounded) instead
  of permanently stopping proactive refresh.
- cli auth: reject flags a subcommand does not accept (e.g. login --watch,
  status --device) and reject empty --scope values — fail fast.

Lint blockers:
- manager: restructured the discovery fallback so the non-nil-err branch no longer
  returns nil (nilerr).
- providers: dropped the ineffectual flow initializer (ineffassign).

Tests: assert Save/Load errors in the scheduler test; the slow_down device test
no longer depends on post-expiry polling. Added tests for reserved-param
stripping, hermetic env, device default-expiry, and CLI flag validation. The
store-path test was already made OS-portable in the Windows-smoke fix.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new)/
govulncheck(0)/deadcode(no new) all pass.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

All 11 findings addressed in 3864909 (replies on each thread):

Security/correctness

  1. flow — reserved OAuth/PKCE params can't be overridden by extras (no plain downgrade / state-redirect rewrite).
  2. device — default bounded expiry when expires_in is absent; poll re-checks expiry after the interval sleep (no post-expiry poll).
  3. lock — WriteString/Close errors handled; no stranded lock.
  4. store — non-nil env map is hermetic (no ambient leak).
  5. scheduler — transient load errors back off + retry (bounded), don't permanently stop.
  6. cli auth — per-subcommand flag rejection + empty-scope rejection.

Lint blockers
7. manager — nilerr cleared (no nil return in the non-nil-err branch).
8. providers — ineffassign cleared (var flow Flow).

Tests — assert Save/Load in the scheduler test; slow_down test no longer depends on post-expiry polling; added tests for reserved-param stripping, hermetic env, device default-expiry, CLI flag validation. The store-path portability fix already landed in d724d9a.

Gates: gofmt · vet · build (host+linux+windows) · test -race · staticcheck (no new) · govulncheck (0) · deadcode (no new) — all green.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

@gnanam1990 Thanks for the thorough summary — addressing all 11 findings including the security/correctness fixes, lint blockers, and added test coverage, with all CI gates green is a solid baseline. Kicking off a full re-review now.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/oauth/scheduler_test.go (1)

17-17: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert seed persistence in the no-op test.

Line 17 ignores the Save error, which can make this test pass on a broken store setup instead of true scheduler no-op behavior.

Suggested fix
-	_ = m.store.Save(ProviderKey("demo"), Token{AccessToken: "a", ExpiresAt: time.Now().Add(time.Hour)})
+	if err := m.store.Save(ProviderKey("demo"), Token{
+		AccessToken: "a",
+		ExpiresAt:   time.Now().Add(time.Hour),
+	}); err != nil {
+		t.Fatalf("seed Save: %v", err)
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/scheduler_test.go` at line 17, The test ignores the error
return value from the m.store.Save call, which means the seed persistence might
silently fail without the test catching it. Replace the underscore blank
identifier with proper error assertion to ensure the Save operation succeeds
before proceeding with the scheduler no-op behavior test. This validates that
the store setup is actually working correctly rather than allowing the test to
pass even if the store is broken.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/oauth/scheduler_test.go`:
- Line 17: The test ignores the error return value from the m.store.Save call,
which means the seed persistence might silently fail without the test catching
it. Replace the underscore blank identifier with proper error assertion to
ensure the Save operation succeeds before proceeding with the scheduler no-op
behavior test. This validates that the store setup is actually working correctly
rather than allowing the test to pass even if the store is broken.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cb44475d-88ca-446a-ad1c-e666377a474b

📥 Commits

Reviewing files that changed from the base of the PR and between d724d9a and 3864909.

📒 Files selected for processing (13)
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/oauth/device.go
  • internal/oauth/device_test.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/lock.go
  • internal/oauth/manager.go
  • internal/oauth/providers.go
  • internal/oauth/scheduler.go
  • internal/oauth/scheduler_test.go
  • internal/oauth/store.go
  • internal/oauth/store_test.go
🚧 Files skipped from review as they are similar to previous changes (11)
  • internal/oauth/device_test.go
  • internal/cli/auth_test.go
  • internal/oauth/lock.go
  • internal/cli/auth.go
  • internal/oauth/device.go
  • internal/oauth/manager.go
  • internal/oauth/scheduler.go
  • internal/oauth/store.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/providers.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
internal/oauth/flow_test.go (1)

109-112: ⚡ Quick win

Isolate the PKCE downgrade assertion with otherwise-valid inputs.

This case currently mixes "plain" with other potentially invalid inputs (missing client_id, non-URL redirect), so future validation-order changes could fail for the wrong reason.

Suggested test tightening
-func TestBuildAuthorizationURLRejectsPlain(t *testing.T) {
-	_, err := BuildAuthorizationURL(Config{AuthorizationEndpoint: "https://a/x"}, PKCE{Method: "plain"}, "s", "r", nil)
+func TestBuildAuthorizationURLRejectsPlain(t *testing.T) {
+	_, err := BuildAuthorizationURL(
+		Config{
+			ClientID:              "client-123",
+			AuthorizationEndpoint: "https://a/x",
+		},
+		PKCE{Method: "plain"},
+		"s",
+		"http://127.0.0.1:9/callback",
+		nil,
+	)
 	if !errors.Is(err, ErrPKCEDowngrade) {
 		t.Fatalf("err = %v, want ErrPKCEDowngrade", err)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/flow_test.go` around lines 109 - 112, The test for
BuildAuthorizationURL in the PKCE downgrade case is mixing the PKCE validation
with other potentially invalid inputs (missing client_id and invalid redirect
URI "r"), which could cause test failures for unrelated reasons if validation
order changes. Modify the test to provide otherwise-valid inputs: ensure the
Config passed to BuildAuthorizationURL includes a valid client_id, pass a
properly formatted redirect URI instead of just "r", and keep Method: "plain" in
the PKCE object. This isolates the test to fail specifically on the PKCE
downgrade issue rather than other validation failures.
internal/oauth/device_test.go (1)

13-51: ⚡ Quick win

Add a request-shape test for device auth client authentication.

Please add a test that verifies RequestDeviceCode sends client_secret when Config.ClientSecret is set. This protects a critical provider-compatibility contract from regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/oauth/device_test.go` around lines 13 - 51, Add a new test function
(named something like TestRequestDeviceCodeWithClientSecret) that verifies the
RequestDeviceCode function sends the client_secret parameter when
Config.ClientSecret is set. The test should set up an httptest server that
inspects the incoming request to verify that client_secret is included in the
request body or query parameters, then call RequestDeviceCode with a Config that
has a non-empty ClientSecret field, and assert that the request validation
passed successfully. This test should follow the same pattern as
TestRequestDeviceCode and TestRequestDeviceCodeDefaultsInterval to ensure the
provider-compatibility contract is protected from regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/cli/auth.go`:
- Around line 336-341: The help text for the authentication command in the Flags
section is missing documentation for the --watch flag, even though it is
referenced in the refresh command. Add the --watch flag to the Flags section in
the help text alongside the other flags (--device, --scope, --json, and -h,
--help) with an appropriate description of what it does (e.g., "Watch for
changes").

In `@internal/oauth/device.go`:
- Around line 58-63: The RequestDeviceCode function constructs a form with
client_id and scope but omits the client_secret, even when it is configured in
cfg. This causes inconsistency with the poll step which does include the
client_secret, and can fail against providers that require client authentication
on the device authorization endpoint. Add the client_secret to the form values
by including a form.Set call for "client_secret" using cfg.ClientSecret,
positioned alongside the existing form.Set calls for "client_id" and "scope".

In `@internal/oauth/loopback.go`:
- Around line 34-43: The NewLoopbackListener function currently accepts an empty
state parameter, which breaks CSRF protection since an empty state would match
any empty state in validation. Add a validation check at the beginning of
NewLoopbackListener to ensure the state parameter is not empty, and return an
error with an appropriate message if the state is empty (for example, check if
len(state) == 0 and return nil, fmt.Errorf with a descriptive message about
requiring a non-empty CSRF state).

In `@internal/oauth/scheduler_test.go`:
- Around line 14-23: In the TestRefreshSchedulerNoRefreshTokenIsNoop function,
the error returned by m.store.Save when saving the token without a refresh token
is currently ignored (assigned to underscore). Instead of ignoring this error,
you should assert that the Save operation succeeds (using a helper like
require.NoError or similar) to ensure the token is actually saved in the store.
This ensures the test validates that the scheduler exits because the token lacks
a refresh token, not because the token was never saved due to a save failure.

---

Nitpick comments:
In `@internal/oauth/device_test.go`:
- Around line 13-51: Add a new test function (named something like
TestRequestDeviceCodeWithClientSecret) that verifies the RequestDeviceCode
function sends the client_secret parameter when Config.ClientSecret is set. The
test should set up an httptest server that inspects the incoming request to
verify that client_secret is included in the request body or query parameters,
then call RequestDeviceCode with a Config that has a non-empty ClientSecret
field, and assert that the request validation passed successfully. This test
should follow the same pattern as TestRequestDeviceCode and
TestRequestDeviceCodeDefaultsInterval to ensure the provider-compatibility
contract is protected from regressions.

In `@internal/oauth/flow_test.go`:
- Around line 109-112: The test for BuildAuthorizationURL in the PKCE downgrade
case is mixing the PKCE validation with other potentially invalid inputs
(missing client_id and invalid redirect URI "r"), which could cause test
failures for unrelated reasons if validation order changes. Modify the test to
provide otherwise-valid inputs: ensure the Config passed to
BuildAuthorizationURL includes a valid client_id, pass a properly formatted
redirect URI instead of just "r", and keep Method: "plain" in the PKCE object.
This isolates the test to fail specifically on the PKCE downgrade issue rather
than other validation failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 07794801-2e60-4ba3-bd65-f18cf5ccb3f1

📥 Commits

Reviewing files that changed from the base of the PR and between 9ddef0e and 3864909.

📒 Files selected for processing (21)
  • internal/cli/app.go
  • internal/cli/auth.go
  • internal/cli/auth_test.go
  • internal/oauth/device.go
  • internal/oauth/device_test.go
  • internal/oauth/discovery.go
  • internal/oauth/flow.go
  • internal/oauth/flow_test.go
  • internal/oauth/lock.go
  • internal/oauth/loopback.go
  • internal/oauth/loopback_test.go
  • internal/oauth/manager.go
  • internal/oauth/manager_test.go
  • internal/oauth/oauth.go
  • internal/oauth/pkce.go
  • internal/oauth/providers.go
  • internal/oauth/providers_test.go
  • internal/oauth/scheduler.go
  • internal/oauth/scheduler_test.go
  • internal/oauth/store.go
  • internal/oauth/store_test.go

Comment thread internal/cli/auth.go
Comment thread internal/oauth/device.go
Comment thread internal/oauth/loopback.go
Comment thread internal/oauth/scheduler_test.go
The 200-way concurrent-send stress test inherited from the swarm package timed
out on Windows CI (103/200 sends hit the 10s lock timeout): the lock's 20ms
retry sleep starves waiters when Windows file ops are slow under heavy
contention. Shorten the retry to 2ms (a freed lock is re-acquired promptly) and
raise the test's lock timeout to 60s so a legitimately-slow CI never times a send
out. Passes repeatedly under -race. (CI de-flake of a test that surfaced on
#210's Windows smoke; functionally unrelated to the OAuth changes.)
gnanam1990 added a commit that referenced this pull request Jun 15, 2026
The 200-way concurrent-send stress test inherited from the swarm package timed
out on Windows CI (103/200 sends hit the 10s lock timeout): the lock's 20ms
retry sleep starves waiters when Windows file ops are slow under heavy
contention. Shorten the retry to 2ms (a freed lock is re-acquired promptly) and
raise the test's lock timeout to 60s so a legitimately-slow CI never times a send
out. Passes repeatedly under -race. (CI de-flake of a test that surfaced on
#210's Windows smoke; functionally unrelated to the OAuth changes.)

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: APPROVED ✅

Reviewed locally in worktree review-pr210 at 19703e5. 3042 additions, 23 files, base mainfeat/oauth-module.

CI gates (clean): go build ./..., go vet ./..., gofmt -l ., go test -count=1 ./internal/oauth/... (7.3s), targeted TestMailboxConcurrentSends re-run (8.7s, no longer flaky on Windows). Full suite shows the same pre-existing TestRunExecUsesProjectConfigAndOpenAICompatibleProvider failure I reproduced on clean main — unrelated to this PR.

Security invariants verified by reading the diff:

  • PKCE S256 mandatory, plain refused via ErrPKCEDowngrade (flow.go:33-40)
  • CSRF state: 32-byte NewState(), verified on callback, never logged
  • Reserved-param guard in BuildAuthorizationURL blocks extra params from overwriting state, redirect_uri, code_challenge[_method], client_id, response_type — prevents caller-supplied params from downgrading PKCE or rewriting the callback target
  • validateTokenEndpoint requires https, with loopback exemption (http://127.0.0.1, http://localhost, or any IsLoopback() IP) — mirrors oauth2-client.js and matches the loopback-callback design
  • Loopback server binds 127.0.0.1 only, OS-assigned port, single-use, idempotent Close so double-close can't race a still-pending Wait
  • RFC 8628 device flow: slow_down adds +5s (per spec), default 10-min expiry if issuer omits it (fail-closed), expiry re-checked AFTER the sleep so a stale token can't slip through on the last poll
  • Token store: regex-bounded keys ^(provider|mcp):[A-Za-z0-9][A-Za-z0-9._-]{0,127}$, schema v1, fail-closed on malformed JSON or wrong schema version, file 0600, dir 0700, atomic write via tmp + rename
  • Cross-process file lock: O_EXCL create, ownership-aware release (read back the token, only delete if it matches), 30s stale detection, partial WriteString/Close errors no longer strand the lock
  • Hermetic env: non-nil env map is authoritative — tests/callers that pass a controlled map cannot pick up ambient HOME / XDG_CONFIG_HOME / ZERO_OAUTH_*
  • Status() never emits token material — redaction-safe by construction
  • Scheduler: jitter on delayUntilRefresh so many sessions don't refresh in lockstep, bounded backoff on consecutive load errors, proactive refresh is an optimization over GetFresh (on-demand is still the source of truth)

The CodeRabbit fixes (commit 3864909) addressed reserved params, hermetic env, default device expiry, lock-write error handling, scheduler retry, and the nilerr lint — all visible in the diff.

One naming nitpick (not a blocker):

In providers.go:91-97, the loop calls validateTokenEndpoint on cfg.IssuerURL and cfg.AuthorizationEndpoint in addition to cfg.TokenEndpoint. The check is correct (every credential- or auth-bearing URL must be https or loopback), but the function name now oversells its scope. Either rename it to validateHttpsOrLoopbackURL or split a validateHttpsURL helper. Cosmetic — happy to follow up in a small refactor PR or just land this as-is.

Overall: clean, well-tested, security-conservative. Merging.

🤖 Generated with Claude Code

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit approvable ?

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current review state and open comments on PR `#210`
gh pr view 210 --repo Gitlawb/zero --json state,reviewDecision,reviews,comments | jq '.'

# Also list review comments still open (not resolved)
gh api repos/Gitlawb/zero/pulls/210/comments --paginate | jq '[.[] | {id, path, line, body: .body[:120], state: (if .pull_request_review_id then "review_comment" else "inline" end)}]' | head -100

Length of output: 110841



@gnanam1990 Not yet — my latest review (from the re-review of commit 3864909) is still CHANGES_REQUESTED with 4 unresolved actionable comments. The human reviewer Vasanthdev2004 has approved, and the automated zero-bot shows no blockers, but two of my open findings are genuine correctness/security items:

# Location Severity Issue
🔴 internal/oauth/loopback.go:34-43 Security NewLoopbackListener accepts an empty state string — an empty state would match any empty state in the CSRF check, breaking callback protection
🟠 internal/oauth/device.go:58-63 Correctness RequestDeviceCode omits client_secret from the device authorization POST even when cfg.ClientSecret is set — inconsistent with the poll step and will fail against providers that require client auth on the device endpoint
🟡 internal/cli/auth.go:336-341 Minor --watch flag is missing from the help text Flags section
🟡 internal/oauth/scheduler_test.go:14-23 Minor store.Save error is silently ignored in the no-op test — a broken store makes the test vacuously pass

The loopback CSRF issue and the missing client_secret in device auth are the two items I'd want addressed before approving. Once those (and ideally the other two) are fixed, I can approve.

- loopback (security): NewLoopbackListener refuses an empty CSRF state — an empty
  state would match a callback carrying no state at all, defeating the check.
- device (correctness): RequestDeviceCode now sends client_secret when set, so a
  confidential client authenticates on the device-authorization endpoint too
  (consistent with the token poll).
- cli auth: document --watch in the help Flags section.
- scheduler test: assert the seed Save error instead of ignoring it.

Also scrubbed a stray third-party reference from a loopback.go comment.
Tests added: empty-state rejection, device client_secret is sent. Gates:
gofmt/vet/build/test -race/staticcheck(no new)/govulncheck(0)/deadcode(no new)
all pass.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

All 4 re-review findings fixed in ac0c659:

# Fix
🔴 loopback CSRF NewLoopbackListener now refuses an empty/whitespace state (fail closed) — an empty state can no longer match a stateless callback. Test: TestNewLoopbackListenerRejectsEmptyState.
🟠 device client_secret RequestDeviceCode now sends client_secret when set, so a confidential client authenticates on the device-authorization endpoint too (consistent with the token poll). Test: TestRequestDeviceCodeSendsClientSecret.
🟡 --watch help Added --watch to the zero auth help Flags section.
🟡 scheduler test The no-op test now asserts the seed Save error instead of ignoring it.

Also scrubbed a leftover third-party reference from a loopback.go comment.

Gates: gofmt · vet · build (host+linux+windows) · test -race · staticcheck (no new) · govulncheck (0) · deadcode (no new) — all green.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check loopback.go for empty state rejection
echo "=== loopback.go NewLoopbackListener ==="
rg -n "state" internal/oauth/loopback.go -A2 -B2 | head -40

# Check device.go for client_secret in RequestDeviceCode
echo ""
echo "=== device.go RequestDeviceCode client_secret ==="
rg -n "client_secret\|ClientSecret" internal/oauth/device.go -A2 -B2 | head -30

# Check auth.go for --watch in help text
echo ""
echo "=== auth.go --watch in help/Flags section ==="
rg -n "watch" internal/cli/auth.go -A2 -B2 | head -30

# Check scheduler_test.go for Save error assertion
echo ""
echo "=== scheduler_test.go Save error assertion ==="
rg -n "Save\|require\|assert" internal/oauth/scheduler_test.go -A2 -B2 | head -40

# Check new loopback test
echo ""
echo "=== loopback_test.go RejectsEmptyState ==="
rg -n "EmptyState\|empty\|whitespace" internal/oauth/loopback_test.go -A5 -B2 | head -30

# Check new device test
echo ""
echo "=== device_test.go SendsClientSecret ==="
rg -n "ClientSecret\|client_secret" internal/oauth/device_test.go -A5 -B2 | head -30

Length of output: 3824


🏁 Script executed:

#!/bin/bash
echo "=== device.go full RequestDeviceCode function ==="
ast-grep --pattern 'func RequestDeviceCode($$$) ($$$) {
  $$$
}'

echo ""
echo "=== scheduler_test.go full content ==="
cat internal/oauth/scheduler_test.go

echo ""
echo "=== loopback_test.go full content ==="
cat internal/oauth/loopback_test.go

echo ""
echo "=== auth.go Flags section in help ==="
rg -n "Flags\|--watch\|watch" internal/cli/auth.go -A1 -B1 | head -50

Length of output: 9694


🏁 Script executed:

#!/bin/bash
echo "=== auth.go help text (renderAuthHelp or similar) ==="
rg -n "Flags\|--watch\|renderHelp\|helpText\|Flags:" internal/cli/auth.go -B1 -A2 | head -60

echo ""
echo "=== auth.go lines around watch in help ==="
grep -n "watch\|Watch" internal/cli/auth.go | head -20

echo ""
echo "=== device_test.go for client_secret test ==="
rg -n "ClientSecret\|client_secret\|Secret" internal/oauth/device_test.go -B2 -A3 | head -40

Length of output: 1462


🏁 Script executed:

#!/bin/bash
echo "=== device_test.go full content ==="
cat internal/oauth/device_test.go

Length of output: 5648



@gnanam1990 All 4 fixes verified in ac0c659:

Finding Fix confirmed Test confirmed
🔴 Loopback CSRF loopback.go L37–38: strings.TrimSpace(state) == "" returns error TestNewLoopbackListenerRejectsEmptyState
🟠 Device client_secret device.go L60–63: form.Set("client_secret", secret) when set TestRequestDeviceCodeSendsClientSecret
🟡 --watch help auth.go L339: --watch Keep the token fresh in the foreground
🟡 Scheduler Save assert scheduler_test.go L17: t.Fatalf("seed Save: %v", err)

Everything looks correct. Approving.

🐇 ✨

[approve]

OAuth Phase 2: the MCP OAuth code now reuses the shared internal/oauth engine
instead of carrying its own copy of the transport/identity-agnostic primitives.
internal/mcp/oauth.go drops ~110 lines (526 -> 418): newPKCE, newState,
discoverAuthorizationServer, the authorize-URL builder, token exchange/refresh,
the token-response parser, and joinWellKnown now delegate to internal/oauth.

Behavior-preserving: MCP keeps its own OAuthConfig/StoredToken types,
LoginOptions/Login orchestration, loopback handling, registerClient (dynamic
client registration — not part of the shared engine), CLI output, and on-disk
token format (mcp-oauth-tokens.json, raw server-name keys). The existing mcp
oauth tests pass UNCHANGED. The only behavioral delta is a hardening inherited
from the shared engine: the token endpoint must be https (loopback exempt) —
a credential is no longer sent to a plaintext non-loopback endpoint.

Stacks on #210 (the internal/oauth package). Gates: gofmt/vet/build(host+
linux+windows)/test -race/staticcheck(no new)/govulncheck(0)/deadcode(no new
vs base) all pass; `zero mcp oauth status` output is identical.
@gnanam1990
gnanam1990 merged commit bb7de29 into main Jun 15, 2026
7 checks passed
gnanam1990 added a commit that referenced this pull request Jun 15, 2026
… daemon (#211)

* feat(daemon): TLS remote bridge — authenticated network access to the daemon

Add internal/daemon/remote: an OPT-IN, TLS-only network bridge on top of the
local daemon. A remote client drives the SAME SessionManager/Pool over the
network behind bearer-token auth, reusing the daemon's framing and dispatch
unchanged. The local Unix-socket daemon stays the default and is unchanged.

- Bridge: per accepted TLS connection, authenticate THEN hand to the daemon's
  control dispatch via the new Server.ServeConn seam — a remote session runs
  under the same sandbox/risk model as a local one (never bypasses it).
- Authenticator + TokenAuthenticator: constant-time bearer-token check, token
  from $ZERO_DAEMON_REMOTE_TOKEN or a file; optional attestation hook (no-op
  default). Auth happens before any control frame; failure closes the connection
  after a small backoff; tokens are never logged.
- RemoteClient (DialRemote): verified TLS dial (never InsecureSkipVerify) + auth
  handshake, then the existing daemon.Client via the new NewClientConn seam, so
  remote and local share one protocol.
- Fail-closed: TLS mandatory (refuses without cert/key), a configurable
  min-protocol-version floor, the 1 MiB frame cap reused, and a bounded
  concurrent-connection cap that refuses overflow.
- CLI: `zero daemon serve-remote --addr --tls-cert --tls-key`; `run`/`attach`
  gain `--remote <addr> [--token --ca-cert --server-name]`.

Daemon seams added (additive, behavior unchanged): Server.ServeConn,
NewClientConn. SessionLink (event POST) and git-bundle repo transfer are
documented follow-ups.

Tests cover auth accept/reject (env + file), TLS-required, cert verification,
version floor, run/status/attach round-trip, re-attach resumption, connection
cap, and ListenAndServeTLS/Close. Gates: gofmt / vet / build (host+linux+
windows) / test -race / staticcheck (no new) / govulncheck (0) / deadcode
(no new) all pass.

* swarm: de-flake TestMailboxConcurrentSends on Windows CI

The 200-way concurrent-send stress test inherited from the swarm package timed
out on Windows CI (103/200 sends hit the 10s lock timeout): the lock's 20ms
retry sleep starves waiters when Windows file ops are slow under heavy
contention. Shorten the retry to 2ms (a freed lock is re-acquired promptly) and
raise the test's lock timeout to 60s so a legitimately-slow CI never times a send
out. Passes repeatedly under -race. (CI de-flake of a test that surfaced on
#210's Windows smoke; functionally unrelated to the OAuth changes.)

* remote: address CodeRabbit re-review (TLS dial + Close idempotency)

- client: replace the deprecated, non-cancellation-aware tls.DialWithDialer with
  a context-aware tls.Dialer.DialContext (resolves the golangci-lint noctx CI
  blocker; the context bounds the dial + TLS handshake).
- bridge: Close now clears b.listener under the lock, so a repeat Close is a
  no-op instead of returning a closed-listener error (matches its doc). Test
  asserts a double Close succeeds.

Gates: gofmt/vet/build(host+linux+windows)/test -race/staticcheck(no new)/
govulncheck(0)/deadcode(no new) all pass.
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.

2 participants