Skip to content

[Bug]: OAuth callback server not stopped after authentication, causing cross-instance CSRF failures #23562

@hchangjae

Description

@hchangjae

Description

The OAuth callback server on port 19876 remains running indefinitely after authenticate() completes. When multiple opencode TUI instances are running, this causes CSRF state validation failures because:

  1. Instance A starts callback server on port 19876 during OAuth flow
  2. Instance A completes authentication — callback server stays running, port stays occupied
  3. Instance B starts, triggers OAuth — ensureRunning() detects port in use, skips server start
  4. Instance B registers its OAuth state in its own process-local pendingAuths Map
  5. Browser callback arrives at Instance A's server — Instance A's pendingAuths is empty
  6. Result: "Invalid or expired state parameter - potential CSRF attack"

Root cause

McpOAuthCallback.stop() is never called after authenticate() completes. The callback server is ephemeral by nature (only needed during the active waitForCallback() window), but it persists for the lifetime of the process.

In oauth-callback.ts, stop() exists but is only called internally when port/path changes during ensureRunning(). No external caller invokes it after auth completion.

Steps to reproduce

  1. Open opencode TUI instance A — it authenticates with a remote OAuth MCP server (callback server starts on port 19876)
  2. Open opencode TUI instance B in another terminal
  3. Instance B attempts OAuth for the same MCP server
  4. Browser opens, user approves — callback goes to Instance A
  5. Instance A logs: oauth callback with invalid state { state: "...", pendingStates: [] }
  6. Instance B times out after 5 minutes

Proposed fix

Add a stopIfIdle() function that stops the callback server only when no pending auth flows remain:

// oauth-callback.ts
export async function stopIfIdle(): Promise<void> {
  if (pendingAuths.size === 0) {
    await stop()
  }
}

Call it after authenticate() resolves (success or failure) in index.ts. This is safe for concurrent auth flows — the server only stops when all pending auths are resolved.

OpenCode version

1.14.19

Operating System

macOS

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions