feat(act): add OAuth 2.1 PKCE support#2
Merged
Merged
Conversation
- New `src/oauth.ts`: full PKCE login flow with local callback server,
cross-platform browser opener, headless stdin fallback, auto token
refresh via discoverOAuthServerInfo + refreshAuthorization, and
disk-backed state in ~/.config/one/oauth-state.json
- `daemon.ts`: add `auth?: "oauth"` to OneActMcpServerConfig; strip it
in normalizeMcpServersForRuntime so it never reaches the transport layer
- `act.ts`:
- `injectOAuthHeaders()` fetches/refreshes tokens and injects
Authorization: Bearer before any server connection (CLI + programmatic
act() and createActSession())
- `act oauth login|logout|status` subcommand
- `act config` wizard: HTTP transport now asks url → headers → OAuth? →
daemon (only when OAuth is not selected; they are mutually exclusive)
- Help text and examples updated to use GitHub Copilot MCP as reference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds OAuth 2.1 PKCE authentication support to the one-act tool so HTTP-based MCP servers can be configured with auth:"oauth", authenticated interactively, and then used with automatic Authorization: Bearer injection (including refresh).
Changes:
- Added a new disk-backed OAuth PKCE implementation with local callback server and token refresh (
packages/one-act/src/oauth.ts). - Extended MCP server config with
auth?: "oauth"and ensured it is stripped before runtime transport config (packages/one-act/src/daemon.ts). - Added
act oauth login|logout|status, updatedact configwizard, and injected OAuth headers for on-demand servers (packages/one-act/src/act.ts).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| packages/one-act/src/oauth.ts | Implements OAuth PKCE flow, token persistence, refresh, and local callback handling. |
| packages/one-act/src/daemon.ts | Adds auth config field and strips it out before passing configs to the runtime transport layer. |
| packages/one-act/src/act.ts | Adds CLI subcommands + config wizard steps and injects OAuth bearer tokens into request headers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+53
to
+56
| function writeOAuthStateFile(file: OAuthStateFile) { | ||
| mkdirSync(dirname(OAUTH_STATE_PATH), { recursive: true }); | ||
| writeFileSync(OAUTH_STATE_PATH, `${JSON.stringify(file, null, 2)}\n`, "utf-8"); | ||
| } |
Comment on lines
+252
to
+261
| function openBrowser(url: string): boolean { | ||
| const cmd = | ||
| process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; | ||
| try { | ||
| const child = spawn(cmd, [url], { detached: true, stdio: "ignore" }); | ||
| child.unref(); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } |
Comment on lines
+326
to
+335
| // result === "REDIRECT": race callback server vs. manual paste (headless fallback). | ||
| // If stdin closes (non-interactive), gracefully fall back to callback-only. | ||
| const stdinPrompt = provider.browserOpened | ||
| ? `If the browser didn't redirect automatically, paste the full redirect URL here and press Enter:\n> ` | ||
| : `Paste the full redirect URL (or just the code=… value) after authorizing, then press Enter:\n> `; | ||
|
|
||
| const stdinRace = waitForCodeFromStdin(stdinPrompt).catch(() => new Promise<string>(() => {})); | ||
| code = await Promise.race([callbackServer.waitForCode(), stdinRace]); | ||
| } finally { | ||
| callbackServer.close(); |
Comment on lines
+202
to
+208
| if (error) { | ||
| res.writeHead(400, { "content-type": "text/html; charset=utf-8" }); | ||
| res.end( | ||
| `<html><body><h1>Authorization failed: ${error}</h1><p>You may close this tab.</p></body></html>`, | ||
| ); | ||
| rejectCode?.(new Error(`OAuth error: ${error}`)); | ||
| return; |
Comment on lines
+1412
to
+1417
| } | ||
|
|
||
| process.stderr.write( | ||
| `Warning: no valid OAuth token for server "${name}". Run: act oauth login ${name}\n`, | ||
| ); | ||
| } |
…leak, warn callback
- Escape HTML in OAuth callback error page (prevent reflected injection)
- Fix Windows browser opener: use `cmd /c start "" <url>` instead of bare `start`
- Suppress async spawn errors via child.on("error", () => {})
- Add AbortSignal to waitForCodeFromStdin; abort controller in runOAuthLogin
so the readline interface is cleaned up when the callback server wins the race
- Make injectOAuthHeaders warn callback optional; only pass process.stderr from
the CLI path so programmatic act()/createActSession() stay side-effect-free
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r known servers GitHub's OAuth server does not support Dynamic Client Registration. Add a KNOWN_CLIENT_IDS map (keyed by hostname) with a pre-registered client_id for api.githubcopilot.com so `act oauth login github` works out of the box without any extra config. - ActOAuthProvider: accept optional clientId; return it from clientInformation() when no saved state, causing the SDK to skip DCR entirely - runOAuthLogin: resolve clientId from arg → KNOWN_CLIENT_IDS → DCR; persist clientInfo after success so token refresh works later - ensureOAuthToken: fall back to KNOWN_CLIENT_IDS when clientInfo not saved - OneActMcpServerConfig: add optional clientId field for custom servers - normalizeMcpServersForRuntime: strip clientId before passing to MCP runtime Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub OAuth Apps require client_secret in the PKCE authorization_code token exchange even when using PKCE — they return incorrect_client_credentials without it. Device Flow explicitly supports no-secret public clients and is the correct flow for CLI tools. - Add DEVICE_FLOW_HOSTS set (api.githubcopilot.com) to route servers that need Device Flow instead of the standard PKCE web callback - Implement runGitHubDeviceFlow: discovers scopes from Protected Resource Metadata, requests device code, polls token endpoint until user authorizes - Clear actionable error when "Enable Device Flow" is not checked in the OAuth App settings - Remove debug fetch wrapper from runOAuthLogin Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/oauth.ts(new): full OAuth 2.1 PKCE login flow — local HTTP callback server, cross-platform browser opener, headless stdin fallback, automatic token refresh, disk-backed state at~/.config/one/oauth-state.jsondaemon.ts: addsauth?: "oauth"field toOneActMcpServerConfig; stripped innormalizeMcpServersForRuntimeso it never leaks to the transport layeract.ts:injectOAuthHeaders()auto-fetches/refreshes tokens and injectsAuthorization: Bearerbefore any server connection — works for CLI,act(), andcreateActSession()act oauth login|logout|statussubcommandact configwizard redesigned for HTTP transports: url → headers → OAuth 2.1 PKCE? → daemon (mutually exclusive with OAuth)https://api.githubcopilot.com/mcp/) as referenceUsage
Test plan
act oauth login <server>opens browser and persists tokenact oauth statusshows token state and expiryact oauth logout <server>clears stored stateact configwizard forstreamable-httpasks OAuth before daemonact()/createActSession()inject token from stored stateauthfield is stripped before reaching the MCP transport🤖 Generated with Claude Code