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
- 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.
- Ensure storedState() is backed by persistent, per-flow storage (cookie, session, file) and is only cleared after a successful token exchange.
- 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
- Keep one OAuth flow per user session at a time
- Store state in durable, session-scoped storage and delete it only after success
- Don't bookmark or replay callback URLs
- Ensure proxies don't rewrite the redirect query string
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
- OAuth authorization response issuer ${callbackIssuer} does n
- OAuth endpoint URL is not allowed: ${endpointUrl.href}
- OAuth protected resource metadata URL ${resourceMetadataUrl.
- Protected resource ${resourceMetadata.resource} does not mat
- Tool approval signature verification failed for approval "${
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/39ba11476446f917.
Report an issue: GitHub.