vercel/ai · error

OAuth state parameter mismatch - possible CSRF attack

Error message

OAuth state parameter mismatch - possible CSRF attack

What it means

On the OAuth callback, the state query parameter returned by the authorization server is compared with the state previously stored via provider.storedState(). A mismatch means the callback did not originate from an authorization request this client started — the classic signature of a CSRF attack on the OAuth redirect — so the code exchange is aborted.

Source

Thrown at packages/mcp/src/tool/oauth.ts:1350

        ...clientMetadata,
        scope: selectedScope,
      },
      fetchFn,
    });

    clientInformation = addAuthorizationServerInformationToClientInformation(
      fullInformation,
      currentAuthorizationServerInformation,
    );
    await provider.saveClientInformation(clientInformation);
  }

  /** On callback, validate state and AS pin before code exchange */
  if (authorizationCode !== undefined) {
    if (provider.storedState) {
      const expectedState = await provider.storedState();
      if (expectedState !== undefined && expectedState !== callbackState) {
        throw new Error(
          'OAuth state parameter mismatch - possible CSRF attack',
        );
      }
    }

    const storedAuthorizationServerInformation =
      await getStoredAuthorizationServerInformation({
        provider,
        clientInformation,
      });
    if (!storedAuthorizationServerInformation) {
      throw new MCPClientOAuthError({
        message:
          'Stored OAuth authorization server metadata is required when exchanging an authorization code',
      });
    }
    validateAuthorizationResponseIssuer({
      callbackIssuer,

View on GitHub (pinned to 69428b1f8b)

Solutions

  1. Restart the OAuth flow: clear stored state, call auth() again without an authorizationCode to get a fresh authorization URL, and complete the redirect in the same session.
  2. Ensure storedState() is backed by persistent, per-flow storage (cookie, session, file) and is only cleared after a successful token exchange.
  3. Check that nothing rewrites the redirect URL query string (proxies, redirects) and that only one OAuth flow runs per user session at a time.

Example fix

// before
const url = new URL(callbackUrl);
await auth(authorizationServerUrl, { ...provider, /* state ignored */ });
// after
const state = new URL(callbackUrl).searchParams.get('state');
const expected = await provider.storedState();
if (expected !== undefined && expected !== state) {
  // restart flow rather than retrying the callback
  return startAuthorization(authorizationServerUrl, provider);
}
Defensive patterns

Strategy: try-catch

Validate before calling

const state = new URL(callbackUrl).searchParams.get('state');
const expected = await provider.storedState?.();
const looksValid = expected === undefined || expected === state;

Try / catch

try {
  await auth(serverUrl, { ...provider, authorizationCode });
} catch (e) {
  if (e instanceof Error && e.message.includes('state parameter mismatch')) {
    // treat as CSRF/replay: clear stored state, restart the authorization flow
    await provider.clearState?.();
    return startNewAuthorization(serverUrl, provider);
  }
  throw e;
}

Prevention

When it happens

Trigger: auth() is called with an authorizationCode (callback phase) and provider.storedState() returns a value that differs from the callbackState extracted from the redirect URL. Also fires if state was consumed/cleared and a stale or attacker-forged callback is replayed, or two concurrent OAuth flows overwrite each other's state.

Common situations: Users clicking an old callback link, browser tab A finishing auth while tab B restarted the flow, server restarts losing stored state, reverse proxies stripping/altering the state query param, or actual CSRF probing of the redirect endpoint.

Related errors


AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30). Data as JSON: /api/errors/39ba11476446f917. Report an issue: GitHub.