ruvnet/ruflo · error · StateMismatchError

state mismatch — the OAuth callback did not match the reques

Error message

state mismatch — the OAuth callback did not match the request this CLI sent

What it means

Thrown by the interactive PKCE login (StateMismatchError) when the state returned by the OAuth callback does not equal the state the CLI generated and embedded in the authorize URL. This is CSRF protection firing: the callback did not correspond to this CLI's outbound request.

Source

Thrown at v3/@claude-flow/cli/src/auth/client.ts:132

  }
}

/** Browser-based loopback PKCE login — the ADR-306 default for an interactive desktop. */
export async function browserLogin(print: (line: string) => void): Promise<LoginResult> {
  const sec = await loadSecurityOAuth();
  const server = await sec.CallbackServer.bind();
  const pkce = sec.generatePkce();
  const url = sec.authorizeUrl(server.redirectUri, pkce.state, pkce.codeChallenge);

  print('Opening your browser to sign in to Cognitum...');
  print(`If it doesn't open automatically, visit:\n\n  ${url}\n`);
  await sec.openBrowser(url).catch(() => {}); // best-effort — the URL above is always the fallback
  print('Waiting for you to finish signing in...');

  const result = await server.awaitCallback();
  const validated = validateCallback(result.error, result.code, result.state, pkce.state);
  if (!validated.ok) {
    if (validated.reason === 'state-mismatch') throw new StateMismatchError();
    throw new LoginDeniedError(validated.detail ?? 'unknown');
  }

  const tokens = await sec.exchangeCode(validated.code, pkce.codeVerifier, server.redirectUri);
  return { tokens, method: 'pkce' };
}

/** Headless fallback: prints the authorize URL with the OOB redirect, prompts for the pasted code. */
export async function manualLogin(
  print: (line: string) => void,
  input: NodeJS.ReadableStream = process.stdin,
): Promise<LoginResult> {
  const sec = await loadSecurityOAuth();
  print('Browser-based callback unavailable (SSH/container detected, or --no-browser).\n');

  const pkce = sec.generatePkce();
  const url = sec.authorizeUrl(sec.OOB_REDIRECT_URI, pkce.state, pkce.codeChallenge);
  print(`Open this URL in a browser and authorize:\n\n  ${url}\n`);

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Close all stale login tabs/windows and start a single fresh `ruflo auth login`.
  2. Ensure no second process is running a login concurrently for the same profile.
  3. Verify the provider's redirect_uri matches the CLI's loopback callback.
Defensive patterns

Strategy: try-catch

Type guard

function isStateMismatch(e: unknown): boolean {
  return e instanceof Error && /state mismatch|did not match the request/.test(e.message);
}

Try / catch

try {
  await interactiveLogin(print);
} catch (e) {
  if (isStateMismatch(e)) {
    // restart the login once with a fresh PKCE pair
  }
  throw e;
}

Prevention

When it happens

Trigger: The provider redirects back with a state that differs from pkce.state: a stale authorize tab from a previous login, a second concurrent login, a replayed/intercepted redirect, or a misconfigured redirect_uri.

Common situations: User completed an older login tab; two `ruflo auth login` runs in parallel; provider redirect_uri does not match the CLI callback server.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/1ba2a262fee66197. Report an issue: GitHub.