Skip to content

feat(cli): add browser-based OAuth2 login with PKCE#1085

Open
jeremyeder wants to merge 3 commits intoambient-code:mainfrom
jeremyeder:worktree-acpctl-browser-login
Open

feat(cli): add browser-based OAuth2 login with PKCE#1085
jeremyeder wants to merge 3 commits intoambient-code:mainfrom
jeremyeder:worktree-acpctl-browser-login

Conversation

@jeremyeder
Copy link
Copy Markdown
Contributor

Summary

  • Adds acpctl login browser subcommand implementing OAuth2 Authorization Code + PKCE flow
  • Opens browser to identity provider (Keycloak/RH SSO), receives callback on local HTTP server, exchanges code for tokens
  • New pkg/oauth/ package with OIDC discovery, PKCE, callback server, and cross-platform browser opener
  • Extends config with refresh_token, issuer_url, client_id fields (env var + config file fallbacks)
  • Supports both automatic browser callback and manual redirect URL paste as fallback
  • 15 unit tests covering all OAuth primitives and callback server scenarios

Test plan

  • go build ./... — compiles cleanly
  • go vet ./... — no issues
  • go test ./pkg/oauth/... — 15/15 pass
  • acpctl login browser --help — correct flags and description
  • Error paths: missing --issuer-url, missing --client-id, unreachable issuer
  • End-to-end: tested against local Keycloak 26.1 with HTTPS + mkcert, full flow from browser open through token exchange to config save

🤖 Generated with Claude Code

Add `acpctl login browser` subcommand that authenticates via OAuth2
Authorization Code + PKCE flow. Opens the user's browser to the identity
provider, receives the callback on a local HTTP server, and exchanges the
code for access and refresh tokens.

- Add pkg/oauth/ package: OIDC discovery, PKCE generation, state
  generation, authorize URL construction, token exchange, local
  callback server, cross-platform browser opener
- Add `login browser` subcommand with --issuer-url, --client-id,
  --scopes flags (env var and config file fallbacks)
- Extend Config with RefreshToken, IssuerURL, ClientID fields
- Support both automatic browser callback and manual redirect URL paste
- 15 tests covering PKCE, state, OIDC discovery, token exchange, and
  callback server scenarios

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Walkthrough

New browser-based OAuth2 Authorization Code + PKCE login for the CLI. Adds an oauth package (OIDC discovery, PKCE, state, token exchange), a local callback HTTP server, a browser subcommand orchestrating the flow, and config fields to persist issuer, client ID, and refresh token.

Changes

Cohort / File(s) Summary
Browser Login Subcommand
components/ambient-cli/cmd/acpctl/login/browser/cmd.go
Implements browser-based OAuth2 login: resolves issuer/client, performs OIDC discovery, generates PKCE and state, starts local callback server, opens browser (prints URL fallback), accepts callback or pasted redirect, exchanges code for tokens, persists access token and oauth config, warns on short expiry.
Login Command Integration
components/ambient-cli/cmd/acpctl/login/cmd.go
Registers the new browser subcommand, updates --token flag help to note browser option, and adjusts runtime validation to suggest acpctl login browser when token is missing.
Config Extensions
components/ambient-cli/pkg/config/config.go
Adds RefreshToken, IssuerURL, ClientID fields; ClearToken() now clears refresh tokens; adds environment-aware getters GetIssuerURL() and GetClientID().
OAuth Core Package
components/ambient-cli/pkg/oauth/oauth.go, components/ambient-cli/pkg/oauth/oauth_test.go
New oauth package: OIDC discovery (/.well-known), PKCE and state generation, authorize URL builder, token exchange (authorization_code + PKCE). Includes unit tests covering success and error cases for discovery, PKCE, state, URL building, and token exchange.
Callback Server
components/ambient-cli/pkg/oauth/callback.go, components/ambient-cli/pkg/oauth/callback_test.go
Local HTTP callback server on 127.0.0.1:0 with /callback handler validating state, handling oauth errors, returning a success page, and sending results via channel; tests for success, state mismatch, missing code, and authorization error.
Browser Opener Utility
components/ambient-cli/pkg/oauth/browser.go
Cross-platform OpenBrowser(url string) error to invoke open (darwin), xdg-open (linux), or rundll32 url.dll,FileProtocolHandler (windows).

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI
    participant IdP
    participant LocalServer
    participant Config

    User->>CLI: run "acpctl login browser"
    CLI->>IdP: OIDC discovery (/.well-known/openid-configuration)
    IdP-->>CLI: authorization_endpoint, token_endpoint
    CLI->>CLI: generate PKCE verifier/challenge and state
    CLI->>LocalServer: start callback server on 127.0.0.1:0
    LocalServer-->>CLI: return listen address
    CLI->>CLI: build authorization URL (with code_challenge, state)
    CLI->>User: open browser / print URL
    User->>IdP: authenticate & authorize (browser)
    IdP->>LocalServer: redirect to /callback?code=...&state=...
    LocalServer->>LocalServer: validate state, extract code
    LocalServer-->>CLI: send Code via channel
    CLI->>IdP: POST token exchange (code, code_verifier)
    IdP-->>CLI: access_token (+ refresh_token)
    CLI->>Config: store access_token, refresh_token, issuer, client_id
    Config-->>CLI: persisted
    CLI-->>User: print success
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the primary change: a browser-based OAuth2 login feature with PKCE support. It is concise, clear, and directly corresponds to the main changeset.
Description check ✅ Passed The description is comprehensive and directly related to the changeset. It outlines the OAuth2 flow implementation, new package structure, configuration extensions, testing, and test plan verification. It clearly communicates the scope and intent of the changes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@jeremyeder
Copy link
Copy Markdown
Contributor Author

This works nicely.

@jeremyeder jeremyeder marked this pull request as ready for review March 28, 2026 03:27
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/ambient-cli/cmd/acpctl/login/browser/cmd.go`:
- Around line 183-186: The current login flow stores tokenResp.RefreshToken into
cfg.RefreshToken and persists the full cfg even though no refresh logic consumes
it; remove writing the refresh token to the normal config to avoid persisting an
unused long‑lived credential. Update the assignment code where cfg.RefreshToken
is set (and any subsequent config persistence path that writes cfg) to skip or
null out cfg.RefreshToken before saving, continue to set
cfg.AccessToken/cfg.IssuerURL/cfg.ClientID as before, and ensure any
Save/WriteConfig call uses the trimmed cfg so refresh tokens are not written to
disk until a proper refresh implementation requires it.
- Around line 139-154: The manual URL paste path currently accepts an OAuth code
even if the "state" query parameter is missing, which bypasses the CSRF/session
check; update the logic in the paste handling (look for parsed, pastedState,
state, manualCh and oauth.CallbackResult in cmd.go) to require a non-empty
pastedState and to return an error (e.g., "URL missing 'state' parameter" or
"state mismatch in pasted URL") if pastedState is empty or does not equal the
expected state, otherwise continue to send the Code in oauth.CallbackResult.

In `@components/ambient-cli/pkg/oauth/oauth.go`:
- Around line 20-23: DiscoverEndpoints currently accepts any non-empty endpoints
from the well-known document; update DiscoverEndpoints to parse and validate the
discovery JSON's "issuer" equals the expected issuer passed into the function
and ensure AuthorizationEndpoint and TokenEndpoint
(OIDCConfig.AuthorizationEndpoint / OIDCConfig.TokenEndpoint) are valid absolute
URLs (scheme present and host non-empty) before returning; if the issuer
mismatches or endpoints are not absolute URLs, return an error and do not use
the discovery document.
- Around line 91-101: BuildAuthorizeURL currently concatenates params to
authEndpoint which breaks when authEndpoint already has query parameters; change
BuildAuthorizeURL to return (string, error), parse authEndpoint with url.Parse
inside BuildAuthorizeURL, merge the new params into the parsed URL's existing
query (use u.Query() and add each key/value from params), set u.RawQuery =
mergedQuery.Encode(), and return u.String(), nil; propagate the new error return
to callers so parse errors are handled.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 90bfd56b-b56a-4df0-bfab-66b9a95fbd88

📥 Commits

Reviewing files that changed from the base of the PR and between 2bc8e00 and 8b1aee4.

📒 Files selected for processing (8)
  • components/ambient-cli/cmd/acpctl/login/browser/cmd.go
  • components/ambient-cli/cmd/acpctl/login/cmd.go
  • components/ambient-cli/pkg/config/config.go
  • components/ambient-cli/pkg/oauth/browser.go
  • components/ambient-cli/pkg/oauth/callback.go
  • components/ambient-cli/pkg/oauth/callback_test.go
  • components/ambient-cli/pkg/oauth/oauth.go
  • components/ambient-cli/pkg/oauth/oauth_test.go

- Require state parameter in manual URL paste path to prevent CSRF bypass
- Stop persisting unused refresh token to config file
- Validate OIDC discovery issuer matches expected issuer (RFC 8414)
- Validate discovery endpoints are absolute URLs
- Use url.Parse for BuildAuthorizeURL to preserve existing query params
- Add TestDiscoverEndpoints_IssuerMismatch test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/ambient-cli/cmd/acpctl/login/browser/cmd.go`:
- Around line 119-132: The stdin-copy goroutine reading from os.Stdin can block
forever if the browser callback finishes first; to fix, add a done channel and,
when the callback path completes, close done and if os.Stdin is a *os.File call
its Close() to unblock the reader; update the anonymous goroutine that writes to
pw to return when done is closed (check done in a non-blocking manner between
reads) and ensure pw.Close() is still deferred so the scanner stops cleanly —
reference the pw writer, the anonymous goroutine that reads os.Stdin, and the
callback/completion code path to wire up done and the conditional
os.Stdin.(*os.File).Close().
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34b32c62-d9d1-49e8-8469-59ea564cea00

📥 Commits

Reviewing files that changed from the base of the PR and between 8b1aee4 and 9754112.

📒 Files selected for processing (3)
  • components/ambient-cli/cmd/acpctl/login/browser/cmd.go
  • components/ambient-cli/pkg/oauth/oauth.go
  • components/ambient-cli/pkg/oauth/oauth_test.go

Comment on lines +119 to +132
// Copy stdin to pipe in background so we can close pr to stop the scanner.
go func() {
defer pw.Close()
buf := make([]byte, 4096)
for {
n, err := os.Stdin.Read(buf)
if n > 0 {
pw.Write(buf[:n]) //nolint:errcheck
}
if err != nil {
return
}
}
}()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Stdin copy goroutine will outlive the function if callback succeeds first.

The goroutine reading from os.Stdin will remain blocked indefinitely if the browser callback completes before any stdin input. While this is a minor leak, it's acceptable for a CLI tool that exits after login. No action required, but worth noting for future refactoring if this code is reused in a long-running context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/ambient-cli/cmd/acpctl/login/browser/cmd.go` around lines 119 -
132, The stdin-copy goroutine reading from os.Stdin can block forever if the
browser callback finishes first; to fix, add a done channel and, when the
callback path completes, close done and if os.Stdin is a *os.File call its
Close() to unblock the reader; update the anonymous goroutine that writes to pw
to return when done is closed (check done in a non-blocking manner between
reads) and ensure pw.Close() is still deferred so the scanner stops cleanly —
reference the pw writer, the anonymous goroutine that reads os.Stdin, and the
callback/completion code path to wire up done and the conditional
os.Stdin.(*os.File).Close().

@ambient-code ambient-code bot added this to the Review Queue milestone Mar 30, 2026
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