decolua/9router · warning · Error

Windsurf callback state mismatch

Error message

Windsurf callback state mismatch

What it means

parseWindsurfCallback throws this when the callback carries a state parameter that does not equal the expectedState generated at flow start. State is CSRF protection: a mismatch means the callback may not belong to the session that initiated the OAuth flow. The library only enforces it when expectedState was supplied and a state param is present.

Source

Thrown at src/lib/oauth/providers/windsurf.js:44

  const text = String(raw || "").trim();
  let queryStr = text;
  if (text.includes("?")) queryStr = text.slice(text.indexOf("?") + 1);
  if (text.startsWith("#")) queryStr = text.slice(1);
  const params = Object.fromEntries(new URLSearchParams(queryStr));
  const pick = (keys) => {
    for (const k of keys) { const v = params[k]; if (v && String(v).trim()) return String(v).trim(); }
    return null;
  };
  const err = pick(["error"]);
  if (err) {
    const desc = pick(["error_description"]);
    throw new Error(desc ? `Windsurf auth failed: ${err} (${desc})` : `Windsurf auth failed: ${err}`);
  }
  const accessToken = pick(["access_token", "token"]);
  if (!accessToken) throw new Error("Windsurf callback missing access_token");
  const state = pick(["state"]);
  if (expectedState && state && state !== expectedState) {
    throw new Error("Windsurf callback state mismatch");
  }
  return { firebaseIdToken: accessToken };
}

// POST RegisterUser {firebase_id_token} → {apiKey, apiServerUrl, name}
async function fetchWindsurfRegisterUser(firebaseIdToken) {
  const data = await windsurfSeatRequest(WINDSURF_CONFIG.registerApiBaseUrl, WINDSURF_CONFIG.registerPath, {
    firebase_id_token: firebaseIdToken,
  });
  const apiKey = extractJsonPath(data, [["apiKey"], ["api_key"]]);
  if (!apiKey) throw new Error("Windsurf RegisterUser missing apiKey");
  const apiServerUrl = extractJsonPath(data, [["apiServerUrl"], ["api_server_url"]]) || WINDSURF_CONFIG.defaultApiServerUrl;
  const name = extractJsonPath(data, [["name"]]);
  return { apiKey, apiServerUrl, name };
}

// Best-effort: GetOneTimeAuthToken → GetCurrentUser → email/name.
async function fetchWindsurfUserInfo(apiServerUrl, firebaseIdToken) {

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Restart the OAuth flow cleanly: open the newly generated auth URL and use only ITS redirect callback
  2. Discard stale tabs / previous authorization URLs; never paste a URL from an earlier attempt
  3. Ensure only one Windsurf connect flow is active at a time (single callback listener/port)
  4. If state was genuinely lost (e.g. a proxy dropped it), verify the token via RegisterUser and treat the flow as unvalidated at your own risk

Example fix

// before: reusing an old callback URL from a previous session
const tokens = await windsurf.exchangeToken(cfg, oldCallbackUrl, redirectUri, null, currentState);
// after: generate a fresh flow each time
const state = crypto.randomBytes(16).toString('hex');
const authUrl = windsurf.buildAuthUrl(cfg, redirectUri, state);
// ... open authUrl, receive callback for THIS state ...
const tokens = await windsurf.exchangeToken(cfg, freshCallbackUrl, redirectUri, null, state);
Defensive patterns

Strategy: validation

Validate before calling

// compare states yourself before invoking the parser for clearer UX
const stateFrom = (raw) => new URLSearchParams(String(raw).split('?')[1] || '').get('state');
const s = stateFrom(callbackUrl);
if (expectedState && s && s !== expectedState) throw new Error('Stale callback — restart the connect flow (state mismatch)');

Type guard

const stateMatches = (raw, expected) => {
  const s = new URLSearchParams(String(raw).split('?')[1] || '').get('state');
  return !s || !expected || s === expected;
};

Try / catch

try { ({ firebaseIdToken } = parseWindsurfCallback(raw, state)); }
catch (e) {
  if (e.message === 'Windsurf callback state mismatch') {
    return restartFlow('Callback from a previous attempt detected — starting a fresh sign-in');
  }
  throw e;
}

Prevention

When it happens

Trigger: Two OAuth flows running concurrently (second callback delivered to the first's listener); user completing a stale auth URL from an earlier attempt; replaying an old callback URL; paste-token flow where state from a different session is pasted.

Common situations: Clicking 'connect' twice and finishing the older browser tab; re-running the flow and pasting a URL saved from the previous attempt; multiple accounts connected in parallel tabs.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/bc11a870222b80ea. Report an issue: GitHub.