tinyhumansai/openhuman · error

OpenRouter OAuth callback state did not match the request.

Error message

OpenRouter OAuth callback state did not match the request.

What it means

Standard OAuth 2.0 CSRF protection: the flow starts with a random state, the provider echoes it on redirect, and extractOAuthCode() compares parsed.searchParams.get('state') against expectedState. A mismatch means the callback belongs to a different authorization attempt than the one this client is currently finishing.

Solutions

  1. Serialize the flow: one OAuth attempt at a time, cancel/close previous tabs and listeners before starting a new one
  2. Keep expectedState in memory tied to the listener handle so a restart creates a fresh pair instead of matching stale callbacks
  3. On mismatch, abort and restart the whole flow (new verifier + state) rather than retrying with the old pair
Defensive patterns

Strategy: try-catch

Validate before calling

// Serialize flows so only one state is ever outstanding:
let inflight: AbortController | null = null;
function beginFlow() {
  inflight?.abort();
  inflight = new AbortController();
  return inflight.signal;
}

Try / catch

try { const code = extractOAuthCode(callbackUrl, state); }
catch (err) {
  if (err instanceof Error && err.message.includes('state did not match')) {
    // stale/foreign callback — ignore it, keep waiting for the current state,
    // or abort and restart the flow with fresh verifier+state
  } else throw err;
}

Prevention

When it happens

Trigger: Two OAuth attempts sharing loopback port 3000 (a stale browser tab completing an old flow while a new one waits); app restart between authorize and callback so expectedState was regenerated/lost; a forged or replayed callback hitting the listener.

Common situations: User re-clicks 'Connect OpenRouter' in a second tab; listener from a previous session still bound; expected state kept in a store that was cleared on reload.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/bb9e85dc80412922. Report an issue: GitHub.

Appendix: source

Thrown at app/src/utils/openrouterOAuth.ts:55

  return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
}

async function createCodeChallenge(verifier: string): Promise<string> {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
  return base64UrlEncode(new Uint8Array(digest));
}

function extractOAuthCode(callbackUrl: string, expectedState: string): string {
  let parsed: URL;
  try {
    parsed = new URL(callbackUrl);
  } catch {
    throw new Error('OpenRouter OAuth returned an invalid callback URL.');
  }

  const actualState = parsed.searchParams.get('state');
  if (actualState !== expectedState) {
    throw new Error('OpenRouter OAuth callback state did not match the request.');
  }

  const code = parsed.searchParams.get('code');
  if (!code) {
    throw new Error('OpenRouter OAuth did not return an authorization code.');
  }
  return code;
}

async function exchangeCodeForKey(
  code: string,
  verifier: string,
  fetchImpl: typeof fetch
): Promise<string> {
  const response = await fetchImpl(OPENROUTER_TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code, code_verifier: verifier, code_challenge_method: PKCE_METHOD }),

View on GitHub (pinned to 7491200858)