OAuth state validation race condition with fast identity provider redirects #6961

Closed
opened 2026-02-16 18:05:43 -05:00 by yindo · 0 comments
Owner

Originally created by @msvechla on GitHub (Jan 20, 2026).

Originally assigned to: @rekram1-node on GitHub.

Description

Description

When authenticating against a remote MCP server that uses OAuth with an identity provider (e.g., Keycloak) where the user already has an active SSO session, the OAuth callback fails with "Invalid or expired state parameter - potential CSRF attack" even though the state parameter is valid.

Root Cause

There's a race condition in the authenticate() function in packages/opencode/src/mcp/index.ts. The OAuth state is only registered in the pendingAuths map when waitForCallback() is called, but this happens after the browser is opened:

// Current flow in authenticate():
const { authorizationUrl } = await startAuth(mcpName)  // generates state, stores to file

// Browser opens here
const subprocess = await open(authorizationUrl)
await new Promise<void>((resolve, reject) => {
  const timeout = setTimeout(() => resolve(), 500)  // 500ms wait
  // ...
})

// State is only added to pendingAuths HERE - AFTER browser opens
const code = await McpOAuthCallback.waitForCallback(oauthState)

When the identity provider has an active SSO session, it immediately redirects back to the callback URL without showing a login screen. This redirect can arrive at the callback server before waitForCallback() has added the state to the pendingAuths map, causing the validation to fail.

Evidence

The state stored in ~/.local/share/opencode/mcp-auth.json matches the state in the callback URL, confirming the state is valid but the in-memory pendingAuths map doesn't have it registered yet when the callback arrives.

Workaround

Log out of the identity provider before authenticating so there's a login screen delay, giving waitForCallback() time to register the state before the callback arrives.

Plugins

No response

OpenCode version

1.1.25

Steps to reproduce

  1. Configure a remote MCP server with OAuth authentication (e.g., Keycloak)
  2. Log into the identity provider in your browser (establish SSO session)
  3. Run opencode mcp auth <server-name> or trigger re-authentication when token expires
  4. The browser opens and immediately redirects back (no login screen due to SSO)
  5. Error: "Invalid or expired state parameter - potential CSRF attack"

Screenshot and/or share link

No response

Operating System

macOS

Terminal

Ghostty

Originally created by @msvechla on GitHub (Jan 20, 2026). Originally assigned to: @rekram1-node on GitHub. ### Description ### Description When authenticating against a remote MCP server that uses OAuth with an identity provider (e.g., Keycloak) where the user already has an active SSO session, the OAuth callback fails with "Invalid or expired state parameter - potential CSRF attack" even though the state parameter is valid. ### Root Cause There's a race condition in the `authenticate()` function in `packages/opencode/src/mcp/index.ts`. The OAuth state is only registered in the `pendingAuths` map when `waitForCallback()` is called, but this happens **after** the browser is opened: ```typescript // Current flow in authenticate(): const { authorizationUrl } = await startAuth(mcpName) // generates state, stores to file // Browser opens here const subprocess = await open(authorizationUrl) await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => resolve(), 500) // 500ms wait // ... }) // State is only added to pendingAuths HERE - AFTER browser opens const code = await McpOAuthCallback.waitForCallback(oauthState) ``` When the identity provider has an active SSO session, it **immediately** redirects back to the callback URL without showing a login screen. This redirect can arrive at the callback server before `waitForCallback()` has added the state to the `pendingAuths` map, causing the validation to fail. ### Evidence The state stored in `~/.local/share/opencode/mcp-auth.json` matches the state in the callback URL, confirming the state is valid but the in-memory `pendingAuths` map doesn't have it registered yet when the callback arrives. ### Workaround Log out of the identity provider before authenticating so there's a login screen delay, giving `waitForCallback()` time to register the state before the callback arrives. ### Plugins _No response_ ### OpenCode version 1.1.25 ### Steps to reproduce 1. Configure a remote MCP server with OAuth authentication (e.g., Keycloak) 2. Log into the identity provider in your browser (establish SSO session) 3. Run `opencode mcp auth <server-name>` or trigger re-authentication when token expires 4. The browser opens and immediately redirects back (no login screen due to SSO) 5. Error: "Invalid or expired state parameter - potential CSRF attack" ### Screenshot and/or share link _No response_ ### Operating System macOS ### Terminal Ghostty
yindo added the bug label 2026-02-16 18:05:43 -05:00
yindo closed this issue 2026-02-16 18:05:44 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: anomalyco/opencode#6961